├── .gemtest ├── .rspec ├── spec ├── helpers │ ├── projects │ │ ├── missing_config │ │ │ └── .gitkeep │ │ ├── bad_config │ │ │ └── config │ │ │ │ └── deploy.yml │ │ ├── missing_dest │ │ │ └── config │ │ │ │ └── deploy.yml │ │ ├── missing_source │ │ │ └── config │ │ │ │ └── deploy.yml │ │ ├── rails │ │ │ └── config │ │ │ │ ├── deploy.yml │ │ │ │ └── deploy │ │ │ │ ├── production.yml │ │ │ │ └── staging.yml │ │ ├── basic │ │ │ └── config │ │ │ │ └── deploy.yml │ │ └── invalid_server │ │ │ └── config │ │ │ └── deploy.yml │ └── projects.rb ├── spec_helper.rb ├── deployml_spec.rb ├── environment_spec.rb ├── project_spec.rb ├── remote_shell_spec.rb └── configuration_spec.rb ├── .document ├── lib ├── deployml │ ├── options.rb │ ├── frameworks.rb │ ├── version.rb │ ├── exceptions │ │ ├── config_not_found.rb │ │ ├── invalid_config.rb │ │ ├── unknown_environment.rb │ │ ├── missing_option.rb │ │ ├── unknown_server.rb │ │ └── unknown_framework.rb │ ├── servers.rb │ ├── frameworks │ │ └── rails.rb │ ├── local_shell.rb │ ├── servers │ │ ├── apache.rb │ │ ├── thin.rb │ │ └── mongrel.rb │ ├── options │ │ ├── mongrel.rb │ │ └── thin.rb │ ├── remote_shell.rb │ ├── shell.rb │ ├── cli.rb │ ├── configuration.rb │ ├── project.rb │ └── environment.rb └── deployml.rb ├── .yardopts ├── bin └── deployml ├── .gitignore ├── gemspec.yml ├── Rakefile ├── LICENSE.txt ├── deployml.gemspec ├── README.md └── ChangeLog.md /.gemtest: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour --format documentation 2 | -------------------------------------------------------------------------------- /spec/helpers/projects/missing_config/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | - 2 | LICENSE.txt 3 | ChangeLog.md 4 | -------------------------------------------------------------------------------- /lib/deployml/options.rb: -------------------------------------------------------------------------------- 1 | require 'deployml/options/thin' 2 | -------------------------------------------------------------------------------- /spec/helpers/projects/bad_config/config/deploy.yml: -------------------------------------------------------------------------------- 1 | not YAML 2 | -------------------------------------------------------------------------------- /lib/deployml/frameworks.rb: -------------------------------------------------------------------------------- 1 | require 'deployml/frameworks/rails' 2 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --markup markdown --title 'DeploYML Documentation' --protected 2 | -------------------------------------------------------------------------------- /lib/deployml.rb: -------------------------------------------------------------------------------- 1 | require 'deployml/project' 2 | require 'deployml/version' 3 | -------------------------------------------------------------------------------- /bin/deployml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'deployml/cli' 4 | 5 | DeploYML::CLI.start 6 | -------------------------------------------------------------------------------- /lib/deployml/version.rb: -------------------------------------------------------------------------------- 1 | module DeploYML 2 | # deploYML version 3 | VERSION = '0.5.4' 4 | end 5 | -------------------------------------------------------------------------------- /spec/helpers/projects/missing_dest/config/deploy.yml: -------------------------------------------------------------------------------- 1 | scm: future_scm 2 | source: user@example.com 3 | -------------------------------------------------------------------------------- /spec/helpers/projects/missing_source/config/deploy.yml: -------------------------------------------------------------------------------- 1 | scm: future_scm 2 | dest: deploy@example.com 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc 2 | pkg 3 | tmp/* 4 | .DS_Store 5 | .yardoc 6 | *.db 7 | *.log 8 | *.swp 9 | *~ 10 | -------------------------------------------------------------------------------- /lib/deployml/exceptions/config_not_found.rb: -------------------------------------------------------------------------------- 1 | module DeploYML 2 | class ConfigNotFound < RuntimeError 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/deployml/exceptions/invalid_config.rb: -------------------------------------------------------------------------------- 1 | module DeploYML 2 | class InvalidConfig < RuntimeError 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | gem 'rspec', '~> 2.4' 2 | require 'rspec' 3 | 4 | require 'deployml/version' 5 | 6 | include DeploYML 7 | -------------------------------------------------------------------------------- /lib/deployml/exceptions/unknown_environment.rb: -------------------------------------------------------------------------------- 1 | module DeploYML 2 | class UnknownEnvironment < RuntimeError 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/helpers/projects/rails/config/deploy.yml: -------------------------------------------------------------------------------- 1 | source: git@github.com:user/project.git 2 | framework: rails 3 | orm: datamapper 4 | -------------------------------------------------------------------------------- /lib/deployml/servers.rb: -------------------------------------------------------------------------------- 1 | require 'deployml/servers/apache' 2 | require 'deployml/servers/mongrel' 3 | require 'deployml/servers/thin' 4 | -------------------------------------------------------------------------------- /spec/helpers/projects/basic/config/deploy.yml: -------------------------------------------------------------------------------- 1 | scm: git 2 | source: git@dev.example.com/var/git/project1.git 3 | dest: deploy@dev.example.com/var/www/project1 4 | -------------------------------------------------------------------------------- /spec/helpers/projects/invalid_server/config/deploy.yml: -------------------------------------------------------------------------------- 1 | scm: git 2 | source: git@example.com 3 | dest: deploy@example.com 4 | server: yet_another_async_web_server 5 | -------------------------------------------------------------------------------- /lib/deployml/exceptions/missing_option.rb: -------------------------------------------------------------------------------- 1 | require 'deployml/exceptions/invalid_config' 2 | 3 | module DeploYML 4 | class MissingOption < InvalidConfig 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/deployml/exceptions/unknown_server.rb: -------------------------------------------------------------------------------- 1 | require 'deployml/exceptions/invalid_config' 2 | 3 | module DeploYML 4 | class UnknownServer < InvalidConfig 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/deployml/exceptions/unknown_framework.rb: -------------------------------------------------------------------------------- 1 | require 'deployml/exceptions/invalid_config' 2 | 3 | module DeploYML 4 | class UnknownFramework < InvalidConfig 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/helpers/projects.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module Helpers 4 | module Projects 5 | PROJECTS_DIR = File.join(File.dirname(__FILE__),'projects') 6 | 7 | def project_dir(name) 8 | File.join(PROJECTS_DIR,name.to_s) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/helpers/projects/rails/config/deploy/production.yml: -------------------------------------------------------------------------------- 1 | dest: 2 | scheme: ssh 3 | user: deploy 4 | host: www.example.com 5 | path: /srv/project 6 | server: 7 | name: thin 8 | options: 9 | config: /etc/thin/example.yml 10 | socket: /tmp/thin.example.sock 11 | -------------------------------------------------------------------------------- /spec/helpers/projects/rails/config/deploy/staging.yml: -------------------------------------------------------------------------------- 1 | dest: 2 | scheme: ssh 3 | user: deploy 4 | host: www.example.com 5 | path: /srv/staging 6 | server: 7 | name: thin 8 | options: 9 | config: /etc/thin/staging.yml 10 | socket: /tmp/thin.staging.sock 11 | -------------------------------------------------------------------------------- /spec/deployml_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'deployml/version' 4 | 5 | describe DeploYML do 6 | it "should have a version" do 7 | @version = DeploYML.const_get('VERSION') 8 | @version.should_not be_nil 9 | @version.should_not be_empty 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /gemspec.yml: -------------------------------------------------------------------------------- 1 | name: deployml 2 | summary: A simple deployment solution that works. 3 | description: 4 | DeploYML is a simple deployment solution that uses a single YAML file, 5 | Git and SSH. 6 | 7 | license: MIT 8 | authors: Postmodern 9 | email: postmodern.mod3@gmail.com 10 | homepage: https://github.com/postmodern/deployml#readme 11 | has_yard: true 12 | 13 | required_ruby_version: ">= 1.8.6" 14 | 15 | dependencies: 16 | addressable: ~> 2.2 17 | rprogram: ~> 0.2 18 | thor: ~> 0.14 19 | 20 | development_dependencies: 21 | rubygems-tasks: ~> 0.1 22 | rspec: ~> 2.4 23 | yard: ~> 0.7 24 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | begin 5 | gem 'rubygems-tasks', '~> 0.1' 6 | require 'rubygems/tasks' 7 | 8 | Gem::Tasks.new 9 | rescue LoadError => e 10 | warn e.message 11 | warn "Run `gem install rubygems-tasks` to install 'rubygems/tasks'." 12 | end 13 | 14 | begin 15 | gem 'rspec', '~> 2.4' 16 | require 'rspec/core/rake_task' 17 | 18 | RSpec::Core::RakeTask.new 19 | rescue LoadError => e 20 | task :spec do 21 | abort "Please run `gem install rspec` to install RSpec." 22 | end 23 | end 24 | task :test => :spec 25 | task :default => :spec 26 | 27 | begin 28 | gem 'yard', '~> 0.7' 29 | require 'yard' 30 | 31 | YARD::Rake::YardocTask.new 32 | rescue LoadError => e 33 | task :yard do 34 | abort "Please run `gem install yard` to install YARD." 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/environment_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'deployml/project' 4 | 5 | describe Environment do 6 | let(:name) { :staging } 7 | subject do 8 | Environment.new(name, { 9 | 'source' => 'git@github.com:user/project.git', 10 | 'dest' => 'ssh://user@www.example.com/srv/project', 11 | 'framework' => 'rails', 12 | 'orm' => 'datamapper', 13 | 'server' => { 14 | 'name' => 'thin', 15 | 'options' => { 16 | 'config' => '/etc/thin/project.yml', 17 | 'socket' => '/tmp/thin.project.sock' 18 | } 19 | } 20 | }) 21 | end 22 | 23 | it "should default 'environment' to the name of the environment" do 24 | subject.environment.should == name 25 | end 26 | 27 | it "should include the framework mixin" do 28 | subject.should be_kind_of(Frameworks::Rails) 29 | end 30 | 31 | it "should include the server mixin" do 32 | subject.should be_kind_of(Servers::Thin) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/deployml/frameworks/rails.rb: -------------------------------------------------------------------------------- 1 | module DeploYML 2 | module Frameworks 3 | # 4 | # Provides methods for deploying Rails projects. 5 | # 6 | module Rails 7 | # 8 | # Overrides the default `rake` method to add a `RAILS_ENV` 9 | # environment variable. 10 | # 11 | # @see {Environment#rake} 12 | # 13 | def rake(task,*arguments) 14 | arguments += ["RAILS_ENV=#{@environment}"] 15 | 16 | super(task,*arguments) 17 | end 18 | 19 | # 20 | # Migrates the database using the `db:autoupgrade` if 21 | # [DataMapper](http://datamapper.org) is being used, or the typical 22 | # `db:migrate` task. 23 | # 24 | # @param [LocalShell, RemoteShell] shell 25 | # The shell to execute commands in. 26 | # 27 | def migrate(shell) 28 | shell.status "Migrating the Database up ..." 29 | shell.ruby 'rake', 'db:migrate', "RAILS_ENV=#{@environment}" 30 | shell.status "Database migrated." 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2012 Hal Brodigan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/deployml/local_shell.rb: -------------------------------------------------------------------------------- 1 | require 'deployml/shell' 2 | 3 | module DeploYML 4 | # 5 | # Represents a shell running on the local system. 6 | # 7 | class LocalShell < Shell 8 | 9 | # 10 | # Runs a program locally. 11 | # 12 | # @param [String] program 13 | # The name or path of the program to run. 14 | # 15 | # @param [Array] arguments 16 | # Additional arguments for the program. 17 | # 18 | def run(program,*arguments) 19 | program = program.to_s 20 | arguments = arguments.map { |arg| arg.to_s } 21 | 22 | system(program,*arguments) 23 | end 24 | 25 | # 26 | # Executes a command. 27 | # 28 | # @param [String] command 29 | # The command to be executed. 30 | # 31 | # @since 0.5.2 32 | # 33 | def exec(command) 34 | system(command) 35 | end 36 | 37 | # 38 | # Prints out a message. 39 | # 40 | # @param [String] message 41 | # The message to print. 42 | # 43 | def echo(message) 44 | puts message 45 | end 46 | 47 | # 48 | # Changes the current working directory. 49 | # 50 | # @param [String] path 51 | # The path of the new current working directory to use. 52 | # 53 | # @yield [] 54 | # If a block is given, then the directory will be changed back after 55 | # the block has returned. 56 | # 57 | def cd(path,&block) 58 | Dir.chdir(path,&block) 59 | end 60 | 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/deployml/servers/apache.rb: -------------------------------------------------------------------------------- 1 | require 'deployml/exceptions/invalid_config' 2 | 3 | module DeploYML 4 | module Servers 5 | # 6 | # Provides methods for starting, stopping and restarting the 7 | # [Apache](http://httpd.apache.org/) web server. 8 | # 9 | module Apache 10 | # 11 | # Starts Apache using the `apachectl start` command. 12 | # 13 | # @param [LocalShell, RemoteShell] shell 14 | # The shell to execute commands in. 15 | # 16 | def server_start(shell) 17 | shell.status "Starting Apache ..." 18 | 19 | shell.run 'apachectl', 'start' 20 | 21 | shell.status "Apache started." 22 | end 23 | 24 | # 25 | # Restarts Apache using the `apachectl restart` command. 26 | # 27 | # @param [LocalShell, RemoteShell] shell 28 | # The shell to execute commands in. 29 | # 30 | def server_restart(shell) 31 | shell.status "Restarting Apache ..." 32 | 33 | shell.run 'apachectl', 'restart' 34 | 35 | shell.status "Apache restarted." 36 | end 37 | 38 | # 39 | # Stops Apache using the `apachectl stop` command. 40 | # 41 | # @param [LocalShell, RemoteShell] shell 42 | # The shell to execute commands in. 43 | # 44 | def server_stop(shell) 45 | shell.status "Stopping Apache ..." 46 | 47 | shell.run 'apachectl', 'stop' 48 | 49 | shell.status "Apache stoped." 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/deployml/options/mongrel.rb: -------------------------------------------------------------------------------- 1 | require 'rprogram/task' 2 | 3 | module DeploYML 4 | module Options 5 | # 6 | # Maps in command-line options for the `mongrel_rails` utility. 7 | # 8 | class Mongrel < RProgram::Task 9 | 10 | # Default options for Mongrel 11 | DEFAULTS = { 12 | :environment => :production, 13 | :address => '127.0.0.1', 14 | :num_servers => 2 15 | } 16 | 17 | long_option :flag => '--environment' 18 | long_option :flag => '--port' 19 | long_option :flag => '--address' 20 | long_option :flag => '--log' 21 | long_option :flag => '--pid' 22 | long_option :flag => '--chdir' 23 | long_option :flag => '--timeout' 24 | long_option :flag => '--throttle' 25 | long_option :flag => '--mime' 26 | long_option :flag => '--root' 27 | long_option :flag => '--num-procs' 28 | long_option :flag => '--debug' 29 | long_option :flag => '--script' 30 | long_option :flag => '--num-servers' 31 | long_option :flag => '--config' 32 | long_option :flag => '--user' 33 | long_option :flag => '--group' 34 | long_option :flag => '--prefix' 35 | long_option :flag => '--help' 36 | long_option :flag => '--version' 37 | 38 | # 39 | # Initialize the Mongrel options. 40 | # 41 | # @param [Hash] options 42 | # The given options. 43 | # 44 | def initialize(options={}) 45 | super(DEFAULTS.merge(options)) 46 | end 47 | 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/project_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'helpers/projects' 3 | 4 | require 'deployml/project' 5 | 6 | describe Project do 7 | include Helpers::Projects 8 | 9 | describe "new" do 10 | it "should find deploy.yml in the 'config/' directory" do 11 | lambda { 12 | Project.new(project_dir(:basic)) 13 | }.should_not raise_error 14 | end 15 | 16 | it "should raise ConfigNotFound when deploy.yml cannot be found" do 17 | lambda { 18 | Project.new(project_dir(:missing_config)) 19 | }.should raise_error(ConfigNotFound) 20 | end 21 | 22 | it "should raise InvalidConfig when deploy.yml does not contain a Hash" do 23 | lambda { 24 | Project.new(project_dir(:bad_config)) 25 | }.should raise_error(InvalidConfig) 26 | end 27 | 28 | it "should raise InvalidConfig if :source is missing" do 29 | lambda { 30 | Project.new(project_dir(:missing_source)) 31 | }.should raise_error(InvalidConfig) 32 | end 33 | 34 | it "should raise InvalidConfig if :dest is missing" do 35 | lambda { 36 | Project.new(project_dir(:missing_dest)) 37 | }.should raise_error(InvalidConfig) 38 | end 39 | 40 | it "should raise InvalidConfig if :server is unknown" do 41 | lambda { 42 | Project.new(project_dir(:invalid_server)) 43 | }.should raise_error(InvalidConfig) 44 | end 45 | 46 | it "should load the :production environment if thats the only env" do 47 | project = Project.new(project_dir(:basic)) 48 | 49 | project.environments.keys.should == [:production] 50 | end 51 | 52 | it "should load multiple environments" do 53 | project = Project.new(project_dir(:rails)) 54 | 55 | project.environments.keys.should =~ [:production, :staging] 56 | end 57 | 58 | it "should load the base config into multiple environments" do 59 | project = Project.new(project_dir(:rails)) 60 | 61 | project.environments.all? { |name,env| 62 | env.framework == :rails && env.orm == :datamapper 63 | }.should == true 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /deployml.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'yaml' 4 | 5 | Gem::Specification.new do |gem| 6 | gemspec = YAML.load_file('gemspec.yml') 7 | 8 | gem.name = gemspec.fetch('name') 9 | gem.version = gemspec.fetch('version') do 10 | lib_dir = File.join(File.dirname(__FILE__),'lib') 11 | $LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir) 12 | 13 | require 'deployml/version' 14 | DeploYML::VERSION 15 | end 16 | 17 | gem.summary = gemspec['summary'] 18 | gem.description = gemspec['description'] 19 | gem.licenses = Array(gemspec['license']) 20 | gem.authors = Array(gemspec['authors']) 21 | gem.email = gemspec['email'] 22 | gem.homepage = gemspec['homepage'] 23 | 24 | glob = lambda { |patterns| gem.files & Dir[*patterns] } 25 | 26 | gem.files = `git ls-files`.split($/) 27 | gem.files = glob[gemspec['files']] if gemspec['files'] 28 | 29 | gem.executables = gemspec.fetch('executables') do 30 | glob['bin/*'].map { |path| File.basename(path) } 31 | end 32 | gem.default_executable = gem.executables.first if Gem::VERSION < '1.7.' 33 | 34 | gem.extensions = glob[gemspec['extensions'] || 'ext/**/extconf.rb'] 35 | gem.test_files = glob[gemspec['test_files'] || '{test/{**/}*_test.rb'] 36 | gem.extra_rdoc_files = glob[gemspec['extra_doc_files'] || '*.{txt,md}'] 37 | 38 | gem.require_paths = Array(gemspec.fetch('require_paths') { 39 | %w[ext lib].select { |dir| File.directory?(dir) } 40 | }) 41 | 42 | gem.requirements = gemspec['requirements'] 43 | gem.required_ruby_version = gemspec['required_ruby_version'] 44 | gem.required_rubygems_version = gemspec['required_rubygems_version'] 45 | gem.post_install_message = gemspec['post_install_message'] 46 | 47 | split = lambda { |string| string.split(/,\s*/) } 48 | 49 | if gemspec['dependencies'] 50 | gemspec['dependencies'].each do |name,versions| 51 | gem.add_dependency(name,split[versions]) 52 | end 53 | end 54 | 55 | if gemspec['development_dependencies'] 56 | gemspec['development_dependencies'].each do |name,versions| 57 | gem.add_development_dependency(name,split[versions]) 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/remote_shell_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'deployml/remote_shell' 3 | 4 | describe RemoteShell do 5 | let(:uri) { Addressable::URI.parse('ssh://deploy@www.example.com/path') } 6 | 7 | subject { RemoteShell.new(uri) } 8 | 9 | it "should parse the given URI" do 10 | subject.uri.should be_kind_of(Addressable::URI) 11 | 12 | subject.uri.user.should == 'deploy' 13 | subject.uri.host.should == 'www.example.com' 14 | subject.uri.path.should == '/path' 15 | end 16 | 17 | describe "#ssh_uri" do 18 | it "should convert normal URIs to SSH URIs" do 19 | subject.ssh_uri.should == 'deploy@www.example.com' 20 | end 21 | 22 | it "must require a URI with a host component" do 23 | bad_uri = Addressable::URI.parse('deploy@www.example.com:/var/www') 24 | shell = RemoteShell.new(bad_uri) 25 | 26 | lambda { 27 | shell.ssh_uri 28 | }.should raise_error(InvalidConfig) 29 | end 30 | end 31 | 32 | it "should enqueue programs to run" do 33 | subject.run 'echo', 'one' 34 | subject.run 'echo', 'two' 35 | 36 | subject.history[0].should == ['echo', 'one'] 37 | subject.history[1].should == ['echo', 'two'] 38 | end 39 | 40 | it "should enqueue echo commands" do 41 | subject.echo 'one' 42 | subject.echo 'two' 43 | 44 | subject.history[0].should == ['echo', 'one'] 45 | subject.history[1].should == ['echo', 'two'] 46 | end 47 | 48 | it "should enqueue directory changes" do 49 | subject.cd '/other' 50 | 51 | subject.history[0].should == ['cd', '/other'] 52 | end 53 | 54 | it "should enqueue temporary directory changes" do 55 | subject.cd '/other' do 56 | subject.run 'pwd' 57 | end 58 | 59 | subject.history[0].should == ['cd', '/other'] 60 | subject.history[1].should == ['pwd'] 61 | subject.history[2].should == ['cd', '-'] 62 | end 63 | 64 | it "should join all commands together into one command" do 65 | subject.run 'echo', 'one' 66 | subject.run 'echo', 'two' 67 | 68 | subject.join.should == 'echo one && echo two' 69 | end 70 | 71 | it "should escape all command arguments" do 72 | subject.run 'program arg1 arg2' 73 | subject.run 'echo', '>>> status' 74 | 75 | subject.join.should == "program arg1 arg2 && echo \\>\\>\\>\\ status" 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/deployml/options/thin.rb: -------------------------------------------------------------------------------- 1 | require 'rprogram/task' 2 | 3 | module DeploYML 4 | module Options 5 | # 6 | # Maps in command-line options for the `thin` utility. 7 | # 8 | class Thin < RProgram::Task 9 | 10 | # Default options for Thin 11 | DEFAULTS = { 12 | :environment => :production, 13 | :address => '127.0.0.1', 14 | :servers => 2 15 | } 16 | 17 | # Server options: 18 | long_option :flag => '--address' 19 | long_option :flag => '--port' 20 | long_option :flag => '--socket' 21 | long_option :flag => '--swiftiply' 22 | long_option :flag => '--adapter' 23 | long_option :flag => '--rackup' 24 | long_option :flag => '--chdir' 25 | long_option :flag => '--stats' 26 | 27 | # Adapter options: 28 | long_option :flag => '--environment' 29 | long_option :flag => '--prefix' 30 | 31 | # Daemon options: 32 | long_option :flag => '--daemonize' 33 | long_option :flag => '--log' 34 | long_option :flag => '--pid' 35 | long_option :flag => '--user' 36 | long_option :flag => '--group' 37 | long_option :flag => '--tag' 38 | 39 | # Cluster options: 40 | long_option :flag => '--servers' 41 | long_option :flag => '--only' 42 | long_option :flag => '--config' 43 | long_option :flag => '--all' 44 | long_option :flag => '--onebyone', :name => :one_by_one 45 | long_option :flag => '--wait' 46 | 47 | # Tuning options: 48 | long_option :flag => '--backend' 49 | long_option :flag => '--timeout' 50 | long_option :flag => '--force' 51 | long_option :flag => '--max-conns', :name => :max_connections 52 | long_option :flag => '--max-persistent-conns', :name => :max_persistant_connections 53 | long_option :flag => '--threaded' 54 | long_option :flag => '--no-epoll' 55 | 56 | # Common options: 57 | long_option :flag => '--require' 58 | long_option :flag => '--debug' 59 | long_option :flag => '--trace' 60 | long_option :flag => '--help' 61 | long_option :flag => '--version' 62 | 63 | # 64 | # Initialize the Thin options. 65 | # 66 | # @param [Hash] options 67 | # The given options. 68 | # 69 | def initialize(options={}) 70 | super(DEFAULTS.merge(options)) 71 | end 72 | 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/deployml/servers/thin.rb: -------------------------------------------------------------------------------- 1 | require 'deployml/exceptions/missing_option' 2 | require 'deployml/options/thin' 3 | 4 | module DeploYML 5 | module Servers 6 | # 7 | # Provides methods for configuring, starting, stopping and restarting 8 | # the [Thin](http://code.macournoyer.com/thin/) web server. 9 | # 10 | module Thin 11 | # 12 | # Initializes options used when calling `thin`. 13 | # 14 | def initialize_server 15 | @thin = Options::Thin.new(@server_options) 16 | @thin.environment ||= @name 17 | end 18 | 19 | # 20 | # Runs a command via the `thin` command. 21 | # 22 | # @param [LocalShell, RemoteShell] shell 23 | # The shell to execute commands in. 24 | # 25 | # @param [Array] arguments 26 | # Additional arguments to call `thin` with. 27 | # 28 | def thin(shell,*arguments) 29 | options = arguments + ['-C', @thin.config, '-s', @thin.servers] 30 | 31 | shell.ruby 'thin', *options 32 | end 33 | 34 | # 35 | # Configures Thin by calling `thin config`. 36 | # 37 | # @param [LocalShell, RemoteShell] shell 38 | # The shell to execute commands in. 39 | # 40 | # @raise [MissingOption] 41 | # No `config` option was listed under the `server` option in the 42 | # `deploy.yml` configuration file. 43 | # 44 | def server_config(shell) 45 | unless @thin.config 46 | raise(MissingOption,"No 'config' option specified under the server options",caller) 47 | end 48 | 49 | shell.status "Configuring Thin ..." 50 | 51 | options = ['-c', shell.uri.path] + @thin.arguments 52 | shell.ruby 'thin', 'config', *options 53 | 54 | shell.status "Thin configured." 55 | end 56 | 57 | # 58 | # Starts Thin by calling `thin start`. 59 | # 60 | # @param [LocalShell, RemoteShell] shell 61 | # The shell to execute commands in. 62 | # 63 | def server_start(shell) 64 | shell.status "Starting Thin ..." 65 | 66 | thin shell, 'start' 67 | 68 | shell.status "Thin started." 69 | end 70 | 71 | # 72 | # Stops Thin by calling `thin stop`. 73 | # 74 | # @param [LocalShell, RemoteShell] shell 75 | # The shell to execute commands in. 76 | # 77 | def server_stop(shell) 78 | shell.status "Stopping Thin ..." 79 | 80 | thin shell, 'stop' 81 | 82 | shell.status "Thin stopped." 83 | end 84 | 85 | # 86 | # Restarts Thin by calling `thin restart`. 87 | # 88 | # @param [LocalShell, RemoteShell] shell 89 | # The shell to execute commands in. 90 | # 91 | def server_restart(shell) 92 | shell.status "Restarting Thin ..." 93 | 94 | thin shell, 'restart' 95 | 96 | shell.status "Thin restarted." 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/deployml/servers/mongrel.rb: -------------------------------------------------------------------------------- 1 | require 'deployml/exceptions/missing_option' 2 | require 'deployml/options/mongrel' 3 | 4 | module DeploYML 5 | module Servers 6 | # 7 | # Provides methods for configuring, starting, stopping and restarting 8 | # the [Mongrel](https://github.com/fauna/mongrel) web server. 9 | # 10 | module Mongrel 11 | # 12 | # Initializes options used when calling `mongrel`. 13 | # 14 | def initialize_server 15 | @mongrel = Options::Mongrel.new(@server_options) 16 | @mongrel.environment ||= @name 17 | end 18 | 19 | # 20 | # Executes a command via the `mongrel_rails` command. 21 | # 22 | # @param [LocalShell, RemoteShell] shell 23 | # The shell to execute commands in. 24 | # 25 | # @param [Array] arguments 26 | # Additional arguments to call `mongrel_rails` with. 27 | # 28 | def mongrel_cluster(shell,*arguments) 29 | options = arguments + ['-c', @mongrel.config] 30 | 31 | shell.ruby 'mongrel_rails', *options 32 | end 33 | 34 | # 35 | # Configures Mongrel by calling `mongrel_rails cluster::configure`. 36 | # 37 | # @param [LocalShell, RemoteShell] shell 38 | # The shell to execute commands in. 39 | # 40 | # @raise [MissingOption] 41 | # No `config` option was listed under the `server` option in the 42 | # `deploy.yml` configuration file. 43 | # 44 | def server_config(shell) 45 | unless @mongrel.config 46 | raise(MissingOption,"No 'config' option specified under server options",caller) 47 | end 48 | 49 | shell.status "Configuring Mongrel ..." 50 | 51 | options = ['-c', shell.uri.path] + @mongrel.arguments 52 | shell.ruby 'mongrel_rails', 'cluster::configure', *options 53 | 54 | shell.status "Mongrel configured." 55 | end 56 | 57 | # 58 | # Starts Mongrel by calling `mongrel_rails cluster::start`. 59 | # 60 | # @param [LocalShell, RemoteShell] shell 61 | # The shell to execute commands in. 62 | # 63 | def server_start(shell) 64 | shell.status "Starting Mongrel(s) ..." 65 | 66 | mongrel_cluster 'cluster::start' 67 | 68 | shell.status "Mongrel(s) started." 69 | end 70 | 71 | # 72 | # Stops Mongrel by calling `mongrel_rails cluster::stop`. 73 | # 74 | # @param [LocalShell, RemoteShell] shell 75 | # The shell to execute commands in. 76 | # 77 | def server_stop(shell) 78 | shell.status "Stopping Mongrel(s) ..." 79 | 80 | mongrel_cluster 'cluster::stop' 81 | 82 | shell.status "Mongrel(s) stopped." 83 | end 84 | 85 | # 86 | # Restarts Mongrel by calling `mongrel_rails cluster::restart`. 87 | # 88 | # @param [LocalShell, RemoteShell] shell 89 | # The shell to execute commands in. 90 | # 91 | def server_restart(shell) 92 | shell.status "Restarting Mongrel(s) ..." 93 | 94 | mongrel_cluster 'cluster::restart' 95 | 96 | shell.status "Mongrel(s) restarted." 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /spec/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'deployml/configuration' 4 | 5 | describe Configuration do 6 | it "should accept String keys" do 7 | config = Configuration.new('orm' => :datamapper) 8 | 9 | config.orm.should == :datamapper 10 | end 11 | 12 | it "should accept Symbol keys" do 13 | config = Configuration.new(:orm => :datamapper) 14 | 15 | config.orm.should == :datamapper 16 | end 17 | 18 | it "should parse 'dest' String URIs" do 19 | config = Configuration.new( 20 | :dest => 'ssh://user@www.example.com/srv/project' 21 | ) 22 | dest = config.dest 23 | 24 | dest.scheme.should == 'ssh' 25 | dest.user.should == 'user' 26 | dest.host.should == 'www.example.com' 27 | dest.path.should == '/srv/project' 28 | end 29 | 30 | it "should parse 'dest' Hash URIs" do 31 | config = Configuration.new(:dest => { 32 | 'scheme' => 'ssh', 33 | 'user' => 'user', 34 | 'host' => 'www.example.com', 35 | 'path' => '/srv/project' 36 | }) 37 | dest = config.dest 38 | 39 | dest.scheme.should == 'ssh' 40 | dest.user.should == 'user' 41 | dest.host.should == 'www.example.com' 42 | dest.path.should == '/srv/project' 43 | end 44 | 45 | it "should parse 'dest' Arrays of URIs" do 46 | config = Configuration.new(:dest => %w[ 47 | ssh://deploy@dev1.example.com/var/www/project1 48 | ssh://deploy@dev2.example.com/var/www/project1 49 | ssh://deploy@dev3.example.com/var/www/project1 50 | ]) 51 | dest = config.dest 52 | 53 | dest[0].host.should == 'dev1.example.com' 54 | dest[1].host.should == 'dev2.example.com' 55 | dest[2].host.should == 'dev3.example.com' 56 | end 57 | 58 | it "should default the 'debug' option to false" do 59 | config = Configuration.new 60 | 61 | config.debug.should == false 62 | end 63 | 64 | it "should default the environment to nil" do 65 | config = Configuration.new 66 | 67 | config.environment.should be_nil 68 | end 69 | 70 | it "should accept a Symbol for the 'server' option" do 71 | config = Configuration.new(:server => :thin) 72 | 73 | config.server_name.should == :thin 74 | config.server_options.should be_empty 75 | end 76 | 77 | it "should accept a Hash for the 'server' option" do 78 | config = Configuration.new( 79 | :server => { 80 | :name => :thin, 81 | :options => {:address => '127.0.0.1'} 82 | } 83 | ) 84 | 85 | config.server_name.should == :thin 86 | config.server_options.should == {:address => '127.0.0.1'} 87 | end 88 | 89 | it "should parse 'before' / 'after' command strings" do 90 | config = Configuration.new( 91 | :before => { 92 | :install => "command1\ncommand2\n", 93 | :update => "command3\ncommand4\n" 94 | } 95 | ) 96 | 97 | config.before[:install][0].should == 'command1' 98 | config.before[:install][1].should == 'command2' 99 | config.before[:update][0].should == 'command3' 100 | config.before[:update][1].should == 'command4' 101 | end 102 | 103 | it "should parse 'before' / 'after' commands" do 104 | config = Configuration.new( 105 | :before => { 106 | :install => %w[command1 command2], 107 | :update => %w[command3 command4] 108 | } 109 | ) 110 | 111 | config.before[:install][0].should == 'command1' 112 | config.before[:install][1].should == 'command2' 113 | config.before[:update][0].should == 'command3' 114 | config.before[:update][1].should == 'command4' 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/deployml/remote_shell.rb: -------------------------------------------------------------------------------- 1 | require 'deployml/exceptions/invalid_config' 2 | require 'deployml/shell' 3 | 4 | require 'addressable/uri' 5 | 6 | module DeploYML 7 | # 8 | # Represents a shell running on a remote server. 9 | # 10 | class RemoteShell < Shell 11 | 12 | # The history of the Remote Shell 13 | attr_reader :history 14 | 15 | # 16 | # Initializes a remote shell session. 17 | # 18 | # @param [Addressable::URI, String] uri 19 | # The URI of the host to connect to. 20 | # 21 | # @param [Environment] environment 22 | # The environment the shell is connected to. 23 | # 24 | # @yield [session] 25 | # If a block is given, it will be passed the new remote shell session. 26 | # 27 | # @yieldparam [ShellSession] session 28 | # The remote shell session. 29 | # 30 | def initialize(uri,environment=nil,&block) 31 | @history = [] 32 | 33 | super(uri,environment,&block) 34 | 35 | replay if block 36 | end 37 | 38 | # 39 | # Enqueues a program to be ran in the session. 40 | # 41 | # @param [String] program 42 | # The name or path of the program to run. 43 | # 44 | # @param [Array] arguments 45 | # Additional arguments for the program. 46 | # 47 | def run(program,*arguments) 48 | @history << [program, *arguments] 49 | end 50 | 51 | # 52 | # Adds a command to be executed. 53 | # 54 | # @param [String] command 55 | # The command string. 56 | # 57 | # @since 0.5.2 58 | # 59 | def exec(command) 60 | @history << [command] 61 | end 62 | 63 | # 64 | # Enqueues an `echo` command to be ran in the session. 65 | # 66 | # @param [String] message 67 | # The message to echo. 68 | # 69 | def echo(message) 70 | run 'echo', message 71 | end 72 | 73 | # 74 | # Enqueues a directory change for the session. 75 | # 76 | # @param [String] path 77 | # The path of the new current working directory to use. 78 | # 79 | # @yield [] 80 | # If a block is given, then the directory will be changed back after 81 | # the block has returned. 82 | # 83 | def cd(path) 84 | @history << ['cd', path] 85 | 86 | if block_given? 87 | yield 88 | @history << ['cd', '-'] 89 | end 90 | end 91 | 92 | # 93 | # Joins the command history together with ` && `, to form a 94 | # single command. 95 | # 96 | # @return [String] 97 | # A single command string. 98 | # 99 | def join 100 | commands = [] 101 | 102 | @history.each do |command| 103 | program = command[0] 104 | arguments = command[1..-1].map { |word| shellescape(word.to_s) } 105 | 106 | commands << [program, *arguments].join(' ') 107 | end 108 | 109 | return commands.join(' && ') 110 | end 111 | 112 | # 113 | # Converts the URI to one compatible with SSH. 114 | # 115 | # @return [String] 116 | # The SSH compatible URI. 117 | # 118 | # @raise [InvalidConfig] 119 | # The URI of the shell does not have a host component. 120 | # 121 | def ssh_uri 122 | unless @uri.host 123 | raise(InvalidConfig,"URI does not have a host: #{@uri}",caller) 124 | end 125 | 126 | new_uri = @uri.host 127 | new_uri = "#{@uri.user}@#{new_uri}" if @uri.user 128 | 129 | return new_uri 130 | end 131 | 132 | # 133 | # Starts a SSH session with the destination server. 134 | # 135 | # @param [Array] arguments 136 | # Additional arguments to pass to SSH. 137 | # 138 | def ssh(*arguments) 139 | options = [] 140 | 141 | # Add the -p option if an alternate destination port is given 142 | if @uri.port 143 | options += ['-p', @uri.port.to_s] 144 | end 145 | 146 | # append the SSH URI 147 | options << ssh_uri 148 | 149 | # append the additional arguments 150 | arguments.each { |arg| options << arg.to_s } 151 | 152 | return system('ssh',*options) 153 | end 154 | 155 | # 156 | # Replays the command history on the remote server. 157 | # 158 | def replay 159 | ssh(self.join) unless @history.empty? 160 | end 161 | 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeploYML 2 | 3 | * [Source](https://github.com/postmodern/deployml) 4 | * [Issues](https://github.com/postmodern/deployml/issues) 5 | * [Documentation](http://rubydoc.info/gems/deployml/frames) 6 | * [Email](mailto:postmodern.mod3 at gmail.com) 7 | 8 | ## Description 9 | 10 | DeploYML is a simple deployment solution that uses a single YAML file, 11 | Git and SSH. 12 | 13 | ## Features 14 | 15 | * Requires only **one YAML file** (`config/deploy.yml`) with a minimum of 16 | **two** settings (`source` and `dest`). 17 | * Supports multiple deployment environments (`config/deploy/staging.yml`). 18 | * Supports multiple deployment destinations. 19 | * Supports [Git](http://www.git-scm.com/). 20 | * Can deploy Ruby web applications or static sites. 21 | * Supports common Web Servers: 22 | * [Apache](http://www.apache.org/) 23 | * [Mongrel](https://github.com/fauna/mongrel) 24 | * [Thin](http://code.macournoyer.com/thin/) 25 | * Supports common Web Application frameworks: 26 | * [Rails](http://rubyonrails.org/): 27 | * [Bundler](http://gembundler.com/) 28 | * ActiveRecord 29 | * [DataMapper](http://datamapper.org/) 30 | * **Does not** require anything else to be installed on the servers. 31 | * **Does not** use `net-ssh`. 32 | * Supports any Operating System that supports Ruby and SSH. 33 | * Provides a simple command-line interface using Thor. 34 | 35 | ## Configuration Examples 36 | 37 | Specifying `source` and `dest` URIs as Strings: 38 | 39 | source: git@github.com:user/project.git 40 | dest: deploy@www.example.com/var/www/site 41 | 42 | Specifying `dest` URI as a Hash: 43 | 44 | source: git@github.com:user/project.git 45 | dest: 46 | user: deploy 47 | host: www.example.com 48 | path: /var/www/site 49 | 50 | Specify multiple `dest` URIs: 51 | 52 | source: git@github.com:user/project.git 53 | dest: 54 | - deploy@www1.example.com/var/www/site 55 | - deploy@www2.example.com/var/www/site 56 | - deploy@www3.example.com/var/www/site 57 | 58 | Specifying a `server` option: 59 | 60 | source: git@github.com:user/project.git 61 | dest: deploy@www.example.com/var/www/site 62 | server: apache 63 | 64 | Specifying a `server` with options: 65 | 66 | source: git@github.com:user/project.git 67 | dest: deploy@www.example.com/var/www/site 68 | server: 69 | name: thin 70 | options: 71 | servers: 4 72 | deamonize: true 73 | socket: /var/run/thin.sock 74 | rackup: true 75 | 76 | Multiple environments: 77 | 78 | # config/deploy.yml 79 | source: git@github.com:user/project.git 80 | framework: rails 81 | orm: datamapper 82 | 83 | # config/deploy/staging.yml 84 | dest: ssh://deploy@www.example.com/srv/staging 85 | server: 86 | name: thin 87 | options: 88 | config: /etc/thin/staging.yml 89 | socket: /tmp/thin.staging.sock 90 | 91 | # config/deploy/production.yml 92 | dest: ssh://deploy@www.example.com/srv/project 93 | server: 94 | name: thin 95 | options: 96 | config: /etc/thin/example.yml 97 | socket: /tmp/thin.example.sock 98 | 99 | Specifying before/after commands: 100 | 101 | before: 102 | restart: rm public/some/file 103 | 104 | after: 105 | install: 106 | - mkdir tmp 107 | - mkdir tmp/pids 108 | - mkdir log 109 | update: rake post_deploy 110 | 111 | ## Synopsis 112 | 113 | Cold-Deploy a new project: 114 | 115 | $ deployml deploy 116 | 117 | Redeploy a project: 118 | 119 | $ deployml redeploy 120 | 121 | Run a rake task on the deploy server: 122 | 123 | $ deployml rake 'db:automigrate' 124 | 125 | Execute a command on the deploy server: 126 | 127 | $ deployml exec 'whoami' 128 | 129 | SSH into the deploy server: 130 | 131 | $ deployml ssh 132 | 133 | List available tasks: 134 | 135 | $ deployml help 136 | 137 | ## Requirements 138 | 139 | * [ruby](http://www.ruby-lang.org/) >= 1.8.6 140 | * [addressable](http://addressable.rubyforge.org/) ~> 2.2 141 | * [rprogram](https://github.com/postmodern/rprogram) ~> 0.2 142 | * [thor](https://github.com/wycats/thor) ~> 0.14 143 | 144 | ## Install 145 | 146 | $ sudo gem install deployml 147 | 148 | ## Copyright 149 | 150 | Copyright (c) 2010-2012 Hal Brodigan 151 | 152 | See {file:LICENSE.txt} for license information. 153 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | ### 0.5.4 / 2012-05-28 2 | 3 | * Fixed a typo in the gemspec, which incorrectly set 4 | `required_rubygems_version` to the same value as `required_ruby_version`. 5 | 6 | ### 0.5.3 / 2012-05-27 7 | 8 | * Require addressable ~> 2.2. 9 | * Require thor ~> 0.14. 10 | 11 | ### 0.5.2 / 2011-06-21 12 | 13 | * Added {DeploYML::Shell#ruby}. 14 | * Override {DeploYML::LocalShell#exec} and {DeploYML::RemoteShell#exec} 15 | to prevent full commands from being escaped. 16 | * Ensure that {DeploYML::Shell#ruby} and {DeploYML::Shell#rake} will 17 | prefix commands with `bundle exec`, if Bundler is enabled. 18 | * All `thin` and `mongrel_cluster` commands now support running under 19 | `bundle exec`. 20 | * Merged `DeploYML::Frameworks::Rails2` and `DeploYML::Frameworks::Rails3` 21 | into {DeploYML::Frameworks::Rails}. 22 | 23 | ### 0.5.1 / 2011-04-25 24 | 25 | * Emergency typo fix for {DeploYML::Environment#invoke}. 26 | 27 | ### 0.5.0 / 2011-04-22 28 | 29 | * Added support for specifying multiple `dest` URIs. 30 | * Added support for specifying `before` and `after` commands. 31 | * Added {DeploYML::Configuration#each_dest}. 32 | * Added {DeploYML::Configuration#normalize}. 33 | * Added {DeploYML::Configuration#normalize_array}. 34 | * Added {DeploYML::Configuration#parse_address}. 35 | * Added {DeploYML::Configuration#parse_dest}. 36 | * Added {DeploYML::Configuration#parse_commands}. 37 | * Added {DeploYML::Environment#invoke_task}. 38 | * Added {DeploYML::Environment#config}. 39 | * Added {DeploYML::Environment#start}. 40 | * Added {DeploYML::Environment#stop}. 41 | * Added {DeploYML::Environment#restart}. 42 | * Added {DeploYML::Shell#uri}. 43 | * Added {DeploYML::Shell#exec}. 44 | * Converted {DeploYML::Shell} into a Class. 45 | * Raise an exception in {DeploYML::RemoteShell#ssh_uri} if the Shell URI 46 | does not have a host component. 47 | 48 | ### 0.4.2 / 2011-04-11 49 | 50 | * Require rprogram ~> 0.2. 51 | * Fixed a typo in `gemspec.yml` which crashed the Psych YAML parser. 52 | * Fixed typos in documentation. 53 | * Opt into [test.rubygems.org](http://test.rubygems.org/) 54 | 55 | ### 0.4.1 / 2010-12-08 56 | 57 | * Added support for Ruby 1.8.6. 58 | * Added {DeploYML::Configuration#bundler}. 59 | * Auto-detect usage of [Bundler](http://gembundler.com/) by checking for a 60 | `Gemfile` in project directories. 61 | * Fixed a Ruby 1.8.x specific bug where non-Strings were being passed to 62 | `Kernel.system`. 63 | * Only print status messages if the mixin is enabled. 64 | 65 | ### 0.4.0 / 2010-11-29 66 | 67 | * Require addressable ~> 2.2.0. 68 | * Added methods to {DeploYML::Environment} inorder to mirror 69 | {DeploYML::Project}: 70 | * `invoke` 71 | * `setup!` 72 | * `update!` 73 | * `install!` 74 | * `migrate!` 75 | * `config!` 76 | * `start!` 77 | * `stop!` 78 | * Added {DeploYML::Shell#status} for printing ANSI colored status messages. 79 | * Added `DeploYML::RemoteShell#uri`. 80 | * Added {DeploYML::RemoteShell#history}. 81 | * Added missing documentation. 82 | * Give the root directory passed to {DeploYML::Project#initialize} the 83 | default of `Dir.pwd`. 84 | * If the destination URI has the scheme of `file:`, have 85 | {DeploYML::Environment#remote_shell} return a {DeploYML::LocalShell}. 86 | * This should facilitate local deploys. 87 | * Perform a forced pull in {DeploYML::Environment#update}. 88 | * Override {DeploYML::Environment#rake} in {DeploYML::Frameworks::Rails}. 89 | * Escape all arguments of all commands in {DeploYML::RemoteShell#join}. 90 | 91 | ### 0.3.0 / 2010-11-21 92 | 93 | * Initial release: 94 | * Requires only **one YAML file** (`config/deploy.yml`) with a minimum of 95 | **two** things (`source` and `dest`). 96 | * Supports multiple deployment environments (`config/deploy/staging.yml`). 97 | * Supports [Git](http://www.git-scm.com/). 98 | * Can deploy Ruby web applications or static sites. 99 | * Supports common Web Servers: 100 | * [Apache](http://www.apache.org/) 101 | * [Mongrel](https://github.com/fauna/mongrel) 102 | * [Thin](http://code.macournoyer.com/thin/) 103 | * Supports common Web Application frameworks: 104 | * [Rails](http://rubyonrails.org/): 105 | * [Bundler](http://gembundler.com/) 106 | * ActiveRecord 107 | * [DataMapper](http://datamapper.org/) 108 | * **Does not** require anything else to be installed on the servers. 109 | * **Does not** use `net-ssh`. 110 | * Supports any Operating System that supports Ruby and SSH. 111 | * Provides a simple command-line interface using Thor. 112 | 113 | -------------------------------------------------------------------------------- /lib/deployml/shell.rb: -------------------------------------------------------------------------------- 1 | require 'thor/shell/color' 2 | require 'shellwords' 3 | 4 | module DeploYML 5 | # 6 | # Provides common methods used by both {LocalShell} and {RemoteShell}. 7 | # 8 | class Shell 9 | 10 | include Thor::Shell 11 | include Shellwords 12 | 13 | # The URI of the Shell. 14 | attr_reader :uri 15 | 16 | # 17 | # Initializes a shell session. 18 | # 19 | # @param [Addressable::URI, String] uri 20 | # The URI of the shell. 21 | # 22 | # @param [Environment] environment 23 | # The environment the shell is connected to. 24 | # 25 | # @yield [session] 26 | # If a block is given, it will be passed the new shell session. 27 | # 28 | # @yieldparam [ShellSession] session 29 | # The shell session. 30 | # 31 | def initialize(uri,environment=nil) 32 | @uri = uri 33 | @environment = environment 34 | 35 | if block_given? 36 | status "Entered #{@uri}." 37 | yield self 38 | status "Leaving #{@uri} ..." 39 | end 40 | end 41 | 42 | # 43 | # Place holder method. 44 | # 45 | # @since 0.5.0 46 | # 47 | def run(program,*arguments) 48 | end 49 | 50 | # 51 | # Place holder method. 52 | # 53 | # @param [String] command 54 | # The command to execute. 55 | # 56 | # @since 0.5.0 57 | # 58 | def exec(command) 59 | end 60 | 61 | # 62 | # Place holder method. 63 | # 64 | # @since 0.5.0 65 | # 66 | def echo(message) 67 | end 68 | 69 | # 70 | # Executes a Ruby program. 71 | # 72 | # @param [Symbol, String] program 73 | # Name of the Ruby program to run. 74 | # 75 | # @param [Array] arguments 76 | # Additional arguments for the Ruby program. 77 | # 78 | # @since 0.5.2 79 | # 80 | def ruby(program,*arguments) 81 | command = [program, *arguments] 82 | 83 | # assume that `.rb` scripts do not have a `#!/usr/bin/env ruby` 84 | command.unshift('ruby') if program[-3,3] == '.rb' 85 | 86 | # if the environment uses bundler, run all ruby commands via `bundle exec` 87 | if (@environment && @environment.bundler) 88 | command.unshift('bundle','exec') 89 | end 90 | 91 | run(*command) 92 | end 93 | 94 | # 95 | # Executes a Rake task. 96 | # 97 | # @param [Symbol, String] task 98 | # Name of the Rake task to run. 99 | # 100 | # @param [Array] arguments 101 | # Additional arguments for the Rake task. 102 | # 103 | def rake(task,*arguments) 104 | ruby('rake', rake_task(task,*arguments)) 105 | end 106 | 107 | # 108 | # Prints a status message. 109 | # 110 | # @param [String] message 111 | # The message to print. 112 | # 113 | # @since 0.4.0 114 | # 115 | def status(message) 116 | echo "#{Color::GREEN}>>> #{message}#{Color::CLEAR}" 117 | end 118 | 119 | protected 120 | 121 | # 122 | # Escapes a string so that it can be safely used in a Bourne shell 123 | # command line. 124 | # 125 | # Note that a resulted string should be used unquoted and is not 126 | # intended for use in double quotes nor in single quotes. 127 | # 128 | # @param [String] str 129 | # The string to escape. 130 | # 131 | # @return [String] 132 | # The shell-escaped string. 133 | # 134 | # @example 135 | # open("| grep #{Shellwords.escape(pattern)} file") { |pipe| 136 | # # ... 137 | # } 138 | # 139 | # @note Vendored from `shellwords.rb` line 72 from Ruby 1.9.2. 140 | # 141 | def shellescape(str) 142 | # An empty argument will be skipped, so return empty quotes. 143 | return "''" if str.empty? 144 | 145 | str = str.dup 146 | 147 | # Process as a single byte sequence because not all shell 148 | # implementations are multibyte aware. 149 | str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1") 150 | 151 | # A LF cannot be escaped with a backslash because a backslash + LF 152 | # combo is regarded as line continuation and simply ignored. 153 | str.gsub!(/\n/, "'\n'") 154 | 155 | return str 156 | end 157 | 158 | # 159 | # Builds a `rake` task name. 160 | # 161 | # @param [String, Symbol] name 162 | # The name of the `rake` task. 163 | # 164 | # @param [Array] arguments 165 | # Additional arguments to pass to the `rake` task. 166 | # 167 | # @return [String] 168 | # The `rake` task name to be called. 169 | # 170 | def rake_task(name,*arguments) 171 | name = name.to_s 172 | 173 | unless arguments.empty? 174 | name += ('[' + arguments.join(',') + ']') 175 | end 176 | 177 | return name 178 | end 179 | 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /lib/deployml/cli.rb: -------------------------------------------------------------------------------- 1 | require 'deployml/project' 2 | 3 | require 'thor' 4 | require 'pathname' 5 | 6 | module DeploYML 7 | # 8 | # The command-line interface to {DeploYML} using 9 | # [Thor](http://github.com/wycats/thor#readme). 10 | # 11 | class CLI < Thor 12 | 13 | namespace 'deploy' 14 | 15 | desc 'exec', 'Runs a command on the deploy server' 16 | method_option :environment, :type => :string, 17 | :default => 'production', 18 | :aliases => '-E' 19 | 20 | # 21 | # Executes a command in the specified environment. 22 | # 23 | # @param [String] command 24 | # The full command to execute. 25 | # 26 | def exec(command) 27 | environment.exec(command) 28 | end 29 | 30 | desc 'rake', 'Executes a rake task on the deploy server' 31 | method_option :environment, :type => :string, 32 | :default => 'production', 33 | :aliases => '-E' 34 | method_option :args, :type => :array 35 | 36 | # 37 | # Invokes a rake task in the specified environment. 38 | # 39 | # @param [String] task 40 | # The name of the rake task. 41 | # 42 | def rake(task) 43 | environment.rake(task,*(options[:args])) 44 | end 45 | 46 | desc 'ssh', 'Starts a SSH session with the deploy server' 47 | method_option :environment, :type => :string, 48 | :default => 'production', 49 | :aliases => '-E' 50 | 51 | # 52 | # Starts an SSH session with the specified environment. 53 | # 54 | def ssh 55 | environment.ssh 56 | end 57 | 58 | desc 'setup', 'Sets up the deployment repository for the project' 59 | method_option :environment, :type => :string, 60 | :default => 'production', 61 | :aliases => '-E' 62 | 63 | # 64 | # Sets up the specified environment. 65 | # 66 | def setup 67 | status 'Setting up ...' 68 | 69 | project.setup!(options[:environment]) 70 | 71 | status 'Setup' 72 | end 73 | 74 | desc 'update', 'Updates the deployment repository of the project' 75 | method_option :environment, :type => :string, 76 | :default => 'production', 77 | :aliases => '-E' 78 | 79 | # 80 | # Updates the deployment repository of the specified environment. 81 | # 82 | def update 83 | status 'Updating' 84 | 85 | project.update!(options[:environment]) 86 | 87 | status 'Updated' 88 | end 89 | 90 | desc 'install', 'Installs the project on the deploy server' 91 | method_option :environment, :type => :string, 92 | :default => 'production', 93 | :aliases => '-E' 94 | 95 | # 96 | # Installs any needed dependencies in the specified environment. 97 | # 98 | def install 99 | status 'Installing ...' 100 | 101 | project.install!(options[:environment]) 102 | 103 | status 'Installed' 104 | end 105 | 106 | desc 'migrate', 'Migrates the database for the project' 107 | method_option :environment, :type => :string, 108 | :default => 'production', 109 | :aliases => '-E' 110 | 111 | # 112 | # Migrates the database for the specified environment. 113 | # 114 | def migrate 115 | status 'Migrating ...' 116 | 117 | project.migrate!(options[:environment]) 118 | 119 | status 'Migrated' 120 | end 121 | 122 | desc 'config', 'Configures the server for the project' 123 | method_option :environment, :type => :string, 124 | :default => 'production', 125 | :aliases => '-E' 126 | 127 | # 128 | # Configures the server for the specified environment. 129 | # 130 | def config 131 | status 'Configuring ...' 132 | 133 | project.config!(options[:environment]) 134 | 135 | status 'Configured' 136 | end 137 | 138 | desc 'start', 'Starts the server for the project' 139 | method_option :environment, :type => :string, 140 | :default => 'production', 141 | :aliases => '-E' 142 | 143 | # 144 | # Starts the server in the specified environment. 145 | # 146 | def start 147 | status 'Starting ...' 148 | 149 | project.start!(options[:environment]) 150 | 151 | status 'Started' 152 | end 153 | 154 | desc 'stop', 'Stops the server for the project' 155 | method_option :environment, :type => :string, 156 | :default => 'production', 157 | :aliases => '-E' 158 | 159 | # 160 | # Stops the server in the specified environment. 161 | # 162 | def stop 163 | status 'Stopping ...' 164 | 165 | project.stop!(options[:environment]) 166 | 167 | status 'Stopped' 168 | end 169 | 170 | desc 'restart', 'Restarts the server for the project' 171 | method_option :environment, :type => :string, 172 | :default => 'production', 173 | :aliases => '-E' 174 | 175 | # 176 | # Restarts the server in the specified environment. 177 | # 178 | def restart 179 | status 'Restarting ...' 180 | 181 | project.restart!(options[:environment]) 182 | 183 | status 'Restarted' 184 | end 185 | 186 | desc 'deploy', 'Cold-Deploys a new project' 187 | method_option :environment, :type => :string, 188 | :default => 'production', 189 | :aliases => '-E' 190 | 191 | # 192 | # Cold-deploys into the specified environment. 193 | # 194 | def deploy 195 | status 'Deploying ...' 196 | 197 | project.deploy!(options[:environment]) 198 | 199 | status 'Deployed' 200 | end 201 | 202 | desc 'redeploy', 'Redeploys the project' 203 | method_option :environment, :type => :string, 204 | :default => 'production', 205 | :aliases => '-E' 206 | 207 | # 208 | # Redeploys into the specified environment. 209 | # 210 | def redeploy 211 | status 'Redeploying ...' 212 | 213 | project.redeploy!(options[:environment]) 214 | 215 | status 'Redeployed' 216 | end 217 | 218 | protected 219 | 220 | # 221 | # Finds the root of the project, starting at the current working 222 | # directory and ascending upwards. 223 | # 224 | # @return [Pathname] 225 | # The root of the project. 226 | # 227 | # @since 0.3.0 228 | # 229 | def find_root 230 | Pathname.pwd.ascend do |root| 231 | config_dir = root.join(Project::CONFIG_DIR) 232 | 233 | if config_dir.directory? 234 | config_file = config_dir.join(Project::CONFIG_FILE) 235 | return root if config_file.file? 236 | 237 | environments_dir = config_dir.join(Project::ENVIRONMENTS_DIR) 238 | return root if environments_dir.directory? 239 | end 240 | end 241 | 242 | shell.say "Could not find '#{Project::CONFIG_FILE}' in any parent directories", :red 243 | exit -1 244 | end 245 | 246 | # 247 | # The project. 248 | # 249 | # @return [Project] 250 | # The project object. 251 | # 252 | # @since 0.3.0 253 | # 254 | def project 255 | @project ||= Project.new(find_root) 256 | end 257 | 258 | # 259 | # The selected environment. 260 | # 261 | # @return [Environment] 262 | # A deployment environment of the project. 263 | # 264 | # @since 0.3.0 265 | # 266 | def environment 267 | project.environment(options[:environment]) 268 | end 269 | 270 | # 271 | # Prints a status message. 272 | # 273 | # @param [String] message 274 | # The message to print. 275 | # 276 | def status(message) 277 | shell.say_status "[#{options[:environment]}]", message 278 | end 279 | 280 | end 281 | end 282 | -------------------------------------------------------------------------------- /lib/deployml/configuration.rb: -------------------------------------------------------------------------------- 1 | require 'deployml/exceptions/missing_option' 2 | require 'deployml/exceptions/invalid_config' 3 | 4 | require 'addressable/uri' 5 | 6 | module DeploYML 7 | # 8 | # The {Configuration} class loads in the settings from a `deploy.yml` 9 | # file. 10 | # 11 | class Configuration 12 | 13 | # Default SCM to use 14 | DEFAULT_SCM = :rsync 15 | 16 | # Valid task names 17 | TASKS = [ 18 | :setup, 19 | :update, 20 | :install, 21 | :migrate, 22 | :config, 23 | :start, 24 | :stop, 25 | :restart 26 | ] 27 | 28 | # The server run the deployed project under 29 | attr_reader :server_name 30 | 31 | # Options for the server 32 | attr_reader :server_options 33 | 34 | # The source URI of the project Git repository. 35 | attr_reader :source 36 | 37 | # The destination URI to upload the project to. 38 | attr_reader :dest 39 | 40 | # Whether the project uses Bundler. 41 | attr_reader :bundler 42 | 43 | # The framework used by the project 44 | attr_reader :framework 45 | 46 | # The ORM used by the project 47 | attr_reader :orm 48 | 49 | # The environment to run the project in 50 | attr_reader :environment 51 | 52 | # Specifies whether to enable debugging. 53 | attr_accessor :debug 54 | 55 | # The arbitrary commands to run before various tasks 56 | attr_reader :before 57 | 58 | # The arbitrary commands to run after various tasks 59 | attr_reader :after 60 | 61 | # 62 | # Creates a new {Configuration} using the given configuration. 63 | # 64 | # @param [Hash] config 65 | # The configuration for the project. 66 | # 67 | # @option config [String] :source 68 | # The source URI of the project Git repository. 69 | # 70 | # @option config [Array, String, Hash] :dest 71 | # The destination URI(s) to upload the project to. 72 | # 73 | # @option config [Boolean] :bundler 74 | # Specifies whether the projects dependencies are controlled by 75 | # [Bundler](http://gembundler.com). 76 | # 77 | # @option config [Symbol] :framework 78 | # The framework used by the project. 79 | # 80 | # @option config [Symbol] :orm 81 | # The ORM used by the project. 82 | # 83 | # @option config [Symbol] :environment 84 | # The environment to run the project in. 85 | # 86 | # @option config [Boolean] :debug (false) 87 | # Specifies whether to enable debugging. 88 | # 89 | # @raise [MissingOption] 90 | # The `server` option Hash did not contain a `name` option. 91 | # 92 | def initialize(config={}) 93 | config = normalize_hash(config) 94 | 95 | @bundler = config.fetch(:bundler,false) 96 | 97 | @framework = case config[:framework] 98 | when 'rails', 'rails2', 'rails3' 99 | :rails 100 | when String, Symbol 101 | config[:framework].to_sym 102 | end 103 | 104 | @orm = if config[:orm] 105 | config[:orm].to_sym 106 | end 107 | 108 | @server_name, @server_options = parse_server(config[:server]) 109 | 110 | @source = config[:source] 111 | @dest = if config[:dest] 112 | parse_dest(config[:dest]) 113 | end 114 | 115 | @environment = if config[:environment] 116 | config[:environment].to_sym 117 | end 118 | 119 | @debug = config.fetch(:debug,false) 120 | 121 | @before = {} 122 | @after = {} 123 | 124 | TASKS.each do |task| 125 | if (config.has_key?(:before) && config[:before].has_key?(task)) 126 | @before[task] = parse_commands(config[:before][task]) 127 | end 128 | 129 | if (config.has_key?(:after) && config[:after].has_key?(task)) 130 | @after[task] = parse_commands(config[:after][task]) 131 | end 132 | end 133 | end 134 | 135 | # 136 | # Iterates over each destination. 137 | # 138 | # @yield [dest] 139 | # The given block will be passed each destination URI. 140 | # 141 | # @yieldparam [Addressable::URI] dest 142 | # A destination URI. 143 | # 144 | # @return [Enumerator] 145 | # If no block is given, an Enumerator object will be returned. 146 | # 147 | # @since 0.5.0 148 | # 149 | def each_dest(&block) 150 | return enum_for(:each_dest) unless block_given? 151 | 152 | if @dest.kind_of?(Array) 153 | @dest.each(&block) 154 | elsif @dest 155 | yield @dest 156 | end 157 | end 158 | 159 | protected 160 | 161 | # 162 | # Normalizes an Array. 163 | # 164 | # @param [Array] array 165 | # The Array to normalize. 166 | # 167 | # @return [Array] 168 | # The normalized Array. 169 | # 170 | # @since 0.5.0 171 | # 172 | def normalize_array(array) 173 | array.map { |value| normalize(value) } 174 | end 175 | 176 | # 177 | # Converts all the keys of a Hash to Symbols. 178 | # 179 | # @param [Hash{Object => Object}] hash 180 | # The hash to be converted. 181 | # 182 | # @return [Hash{Symbol => Object}] 183 | # The normalized Hash. 184 | # 185 | def normalize_hash(hash) 186 | new_hash = {} 187 | 188 | hash.each do |key,value| 189 | new_hash[key.to_sym] = normalize(value) 190 | end 191 | 192 | return new_hash 193 | end 194 | 195 | # 196 | # Normalizes a value. 197 | # 198 | # @param [Hash, Array, Object] value 199 | # The value to normalize. 200 | # 201 | # @return [Hash, Array, Object] 202 | # The normalized value. 203 | # 204 | # @since 0.5.0 205 | # 206 | def normalize(value) 207 | case value 208 | when Hash 209 | normalize_hash(value) 210 | when Array 211 | normalize_array(value) 212 | else 213 | value 214 | end 215 | end 216 | 217 | # 218 | # Parses the value for the `server` setting. 219 | # 220 | # @return [Array] 221 | # The name of the server and additional options. 222 | # 223 | # @since 0.5.0 224 | # 225 | def parse_server(server) 226 | name = nil 227 | options = {} 228 | 229 | case server 230 | when Symbol, String 231 | name = server.to_sym 232 | when Hash 233 | unless server.has_key?(:name) 234 | raise(MissingOption,"the 'server' option must contain a 'name' option for which server to use",caller) 235 | end 236 | 237 | if server.has_key?(:name) 238 | name = server[:name].to_sym 239 | end 240 | 241 | if server.has_key?(:options) 242 | options.merge!(server[:options]) 243 | end 244 | end 245 | 246 | return [name, options] 247 | end 248 | 249 | # 250 | # Parses an address. 251 | # 252 | # @param [Hash, String] address 253 | # The address to parse. 254 | # 255 | # @return [Addressable::URI] 256 | # The parsed address. 257 | # 258 | # @since 0.5.0 259 | # 260 | def parse_address(address) 261 | case address 262 | when Hash 263 | Addressable::URI.new(address) 264 | when String 265 | Addressable::URI.parse(address) 266 | else 267 | raise(InvalidConfig,"invalid address: #{address.inspect}",caller) 268 | end 269 | end 270 | 271 | # 272 | # Parses the value for the `dest` setting. 273 | # 274 | # @param [Array, Hash, String] dest 275 | # The value of the `dest` setting. 276 | # 277 | # @return [Array, Addressable::URI] 278 | # The parsed `dest` value. 279 | # 280 | # @since 0.5.0 281 | # 282 | def parse_dest(dest) 283 | case dest 284 | when Array 285 | dest.map { |address| parse_address(address) } 286 | else 287 | parse_address(dest) 288 | end 289 | end 290 | 291 | # 292 | # Parses a command. 293 | # 294 | # @param [Array, String] command 295 | # The command or commands to parse. 296 | # 297 | # @return [Array] 298 | # The individual commands. 299 | # 300 | # @raise [InvalidConfig] 301 | # The command must be either an Array of a String. 302 | # 303 | # @since 0.5.0 304 | # 305 | def parse_commands(command) 306 | case command 307 | when Array 308 | command.map { |line| line.to_s } 309 | when String 310 | command.enum_for(:each_line).map { |line| line.chomp } 311 | else 312 | raise(InvalidConfig,"commands must be an Array or a String") 313 | end 314 | end 315 | 316 | end 317 | end 318 | -------------------------------------------------------------------------------- /lib/deployml/project.rb: -------------------------------------------------------------------------------- 1 | require 'deployml/exceptions/config_not_found' 2 | require 'deployml/exceptions/invalid_config' 3 | require 'deployml/exceptions/unknown_environment' 4 | require 'deployml/environment' 5 | require 'deployml/remote_shell' 6 | 7 | require 'yaml' 8 | 9 | module DeploYML 10 | class Project 11 | 12 | # The general configuration directory. 13 | CONFIG_DIR = 'config' 14 | 15 | # The configuration file name. 16 | CONFIG_FILE = 'deploy.yml' 17 | 18 | # The configuration directory. 19 | ENVIRONMENTS_DIR = 'deploy' 20 | 21 | # The name of the directory to stage deployments in. 22 | STAGING_DIR = '.deploy' 23 | 24 | # The root directory of the project 25 | attr_reader :root 26 | 27 | # The deployment environments of the project 28 | attr_reader :environments 29 | 30 | # 31 | # Creates a new project using the given configuration file. 32 | # 33 | # @param [String] root 34 | # The root directory of the project. 35 | # 36 | # @raise [ConfigNotFound] 37 | # The configuration file for the project could not be found 38 | # in any of the common directories. 39 | # 40 | def initialize(root=Dir.pwd) 41 | @root = File.expand_path(root) 42 | @config_file = File.join(@root,CONFIG_DIR,CONFIG_FILE) 43 | @environments_dir = File.join(@root,CONFIG_DIR,ENVIRONMENTS_DIR) 44 | 45 | unless (File.file?(@config_file) || File.directory?(@environments_dir)) 46 | raise(ConfigNotFound,"could not find '#{CONFIG_FILE}' or '#{ENVIRONMENTS_DIR}' in #{root}",caller) 47 | end 48 | 49 | load_environments! 50 | end 51 | 52 | # 53 | # @param [Symbol, String] name 54 | # The name of the environment to use. 55 | # 56 | # @return [Environment] 57 | # The environment with the given name. 58 | # 59 | # @raise [UnknownEnvironment] 60 | # No environment was configured with the given name. 61 | # 62 | # @since 0.3.0 63 | # 64 | def environment(name=:production) 65 | name = name.to_sym 66 | 67 | unless @environments[name] 68 | raise(UnknownEnvironment,"unknown environment: #{name}",caller) 69 | end 70 | 71 | return @environments[name] 72 | end 73 | 74 | # 75 | # Convenience method for accessing the development environment. 76 | # 77 | # @return [Environment] 78 | # The development environment. 79 | # 80 | # @since 0.3.0 81 | # 82 | def development 83 | environment(:development) 84 | end 85 | 86 | # 87 | # Convenience method for accessing the staging environment. 88 | # 89 | # @return [Environment] 90 | # The staging environment. 91 | # 92 | # @since 0.3.0 93 | # 94 | def staging 95 | environment(:staging) 96 | end 97 | 98 | # 99 | # Convenience method for accessing the production environment. 100 | # 101 | # @return [Environment] 102 | # The production environment. 103 | # 104 | # @since 0.3.0 105 | # 106 | def production 107 | environment(:production) 108 | end 109 | 110 | # 111 | # Deploys the project. 112 | # 113 | # @param [Array] tasks 114 | # The tasks to run during the deployment. 115 | # 116 | # @param [Symbol, String] env 117 | # The environment to deploy to. 118 | # 119 | # @return [true] 120 | # Indicates that the tasks were successfully completed. 121 | # 122 | # @since 0.2.0 123 | # 124 | def invoke(tasks,env=:production) 125 | environment(env).invoke(tasks) 126 | end 127 | 128 | # 129 | # Sets up the deployment repository for the project. 130 | # 131 | # @param [Symbol, String] env 132 | # The environment to deploy to. 133 | # 134 | # @return [true] 135 | # Indicates that the tasks were successfully completed. 136 | # 137 | def setup!(env=:production) 138 | environment(env).setup! 139 | end 140 | 141 | # 142 | # Updates the deployed repository of the project. 143 | # 144 | # @param [Symbol, String] env 145 | # The environment to deploy to. 146 | # 147 | # @return [true] 148 | # Indicates that the tasks were successfully completed. 149 | # 150 | def update!(env=:production) 151 | environment(env).update! 152 | end 153 | 154 | # 155 | # Installs the project on the destination server. 156 | # 157 | # @param [Symbol, String] env 158 | # The environment to deploy to. 159 | # 160 | # @return [true] 161 | # Indicates that the tasks were successfully completed. 162 | # 163 | def install!(env=:production) 164 | environment(env).install! 165 | end 166 | 167 | # 168 | # Migrates the database used by the project. 169 | # 170 | # @param [Symbol, String] env 171 | # The environment to deploy to. 172 | # 173 | # @return [true] 174 | # Indicates that the tasks were successfully completed. 175 | # 176 | def migrate!(env=:production) 177 | environment(env).migrate! 178 | end 179 | 180 | # 181 | # Configures the Web server to be ran on the destination server. 182 | # 183 | # @param [Symbol, String] env 184 | # The environment to deploy to. 185 | # 186 | # @return [true] 187 | # Indicates that the tasks were successfully completed. 188 | # 189 | def config!(env=:production) 190 | environment(env).config! 191 | end 192 | 193 | # 194 | # Starts the Web server for the project. 195 | # 196 | # @param [Symbol, String] env 197 | # The environment to deploy to. 198 | # 199 | # @return [true] 200 | # Indicates that the tasks were successfully completed. 201 | # 202 | def start!(env=:production) 203 | environment(env).start! 204 | end 205 | 206 | # 207 | # Stops the Web server for the project. 208 | # 209 | # @param [Symbol, String] env 210 | # The environment to deploy to. 211 | # 212 | # @return [true] 213 | # Indicates that the tasks were successfully completed. 214 | # 215 | def stop!(env=:production) 216 | environment(env).stop! 217 | end 218 | 219 | # 220 | # Restarts the Web server for the project. 221 | # 222 | # @param [Symbol, String] env 223 | # The environment to deploy to. 224 | # 225 | # @return [true] 226 | # Indicates that the tasks were successfully completed. 227 | # 228 | def restart!(env=:production) 229 | environment(env).restart! 230 | end 231 | 232 | # 233 | # Deploys a new project. 234 | # 235 | # @param [Symbol, String] env 236 | # The environment to deploy to. 237 | # 238 | # @return [true] 239 | # Indicates that the tasks were successfully completed. 240 | # 241 | # @since 0.2.0 242 | # 243 | def deploy!(env=:production) 244 | environment(env).deploy! 245 | end 246 | 247 | # 248 | # Redeploys a project. 249 | # 250 | # @param [Symbol, String] env 251 | # The environment to deploy to. 252 | # 253 | # @return [true] 254 | # Indicates that the tasks were successfully completed. 255 | # 256 | # @since 0.2.0 257 | # 258 | def redeploy!(env=:production) 259 | environment(env).redeploy! 260 | end 261 | 262 | protected 263 | 264 | # 265 | # Infers the configuration from the project root directory. 266 | # 267 | # @return [Hash{Symbol => Object}] 268 | # The inferred configuration. 269 | # 270 | # @since 0.4.1 271 | # 272 | def infer_configuration 273 | config = {} 274 | 275 | # check for Bundler 276 | if File.file?(File.join(@root,'Gemfile')) 277 | config[:bundler] = true 278 | end 279 | 280 | return config 281 | end 282 | 283 | # 284 | # Loads configuration from a YAML file. 285 | # 286 | # @param [String] path 287 | # The path to the configuration file. 288 | # 289 | # @return [Hash] 290 | # The loaded configuration. 291 | # 292 | # @raise [InvalidConfig] 293 | # The configuration file did not contain a YAML Hash. 294 | # 295 | # @since 0.4.1 296 | # 297 | def load_configuration(path) 298 | config = YAML.load_file(path) 299 | 300 | unless config.kind_of?(Hash) 301 | raise(InvalidConfig,"DeploYML file #{path.dump} does not contain a Hash",caller) 302 | end 303 | 304 | return config 305 | end 306 | 307 | # 308 | # Loads the project configuration. 309 | # 310 | # @since 0.3.0 311 | # 312 | def load_environments! 313 | base_config = infer_configuration 314 | 315 | if File.file?(@config_file) 316 | base_config.merge!(load_configuration(@config_file)) 317 | end 318 | 319 | @environments = {} 320 | 321 | if File.directory?(@environments_dir) 322 | Dir.glob(File.join(@environments_dir,'*.yml')) do |path| 323 | config = base_config.merge(load_configuration(path)) 324 | name = File.basename(path).sub(/\.yml$/,'').to_sym 325 | 326 | @environments[name] = Environment.new(name,config) 327 | end 328 | else 329 | @environments[:production] = Environment.new(:production,base_config) 330 | end 331 | end 332 | 333 | end 334 | end 335 | -------------------------------------------------------------------------------- /lib/deployml/environment.rb: -------------------------------------------------------------------------------- 1 | require 'deployml/exceptions/missing_option' 2 | require 'deployml/exceptions/unknown_server' 3 | require 'deployml/exceptions/unknown_framework' 4 | require 'deployml/configuration' 5 | require 'deployml/local_shell' 6 | require 'deployml/remote_shell' 7 | require 'deployml/servers' 8 | require 'deployml/frameworks' 9 | 10 | module DeploYML 11 | # 12 | # Contains environment specific configuration loaded by {Project} 13 | # from YAML files within `config/deploy/`. 14 | # 15 | class Environment < Configuration 16 | 17 | # Mapping of possible 'server' names to their mixins. 18 | SERVERS = { 19 | :apache => Servers::Apache, 20 | :mongrel => Servers::Mongrel, 21 | :thin => Servers::Thin 22 | } 23 | 24 | # Mapping of possible 'framework' names to their mixins. 25 | FRAMEWORKS = { 26 | :rails => Frameworks::Rails 27 | } 28 | 29 | # 30 | # Creates a new deployment environment. 31 | # 32 | # @param [Symbol, String] name 33 | # The name of the deployment environment. 34 | # 35 | # @param [Hash{String => Object}] config 36 | # Environment specific configuration. 37 | # 38 | # @raise [MissingOption] 39 | # Either the `source` or `dest` options were not specified in the 40 | # confirmation. 41 | # 42 | # @since 0.3.0 43 | # 44 | def initialize(name,config={}) 45 | super(config) 46 | 47 | unless @source 48 | raise(MissingOption,":source option is missing for the #{@name} environment",caller) 49 | end 50 | 51 | unless @dest 52 | raise(MissingOption,":dest option is missing for the #{@name} environment",caller) 53 | end 54 | 55 | @environment ||= name.to_sym 56 | 57 | load_framework! 58 | load_server! 59 | end 60 | 61 | # 62 | # Creates a local shell. 63 | # 64 | # @yield [shell] 65 | # If a block is given, it will be passed the new local shell. 66 | # 67 | # @yieldparam [LocalShell] shell 68 | # The remote shell session. 69 | # 70 | # @return [Array] 71 | # The local shell. 72 | # 73 | # @since 0.3.0 74 | # 75 | def local_shell(&block) 76 | each_dest.map { |dest| LocalShell.new(dest,self,&block) } 77 | end 78 | 79 | # 80 | # Creates a remote shell with the destination server. 81 | # 82 | # @yield [shell] 83 | # If a block is given, it will be passed the new remote shell. 84 | # 85 | # @yieldparam [LocalShell, RemoteShell] shell 86 | # The remote shell. 87 | # 88 | # @return [Array] 89 | # The remote shell. If the destination is a local `file://` URI, 90 | # a local shell will be returned instead. 91 | # 92 | # @since 0.3.0 93 | # 94 | def remote_shell(&block) 95 | each_dest.map do |dest| 96 | shell = if dest.scheme == 'file' 97 | LocalShell 98 | else 99 | RemoteShell 100 | end 101 | 102 | shell.new(dest,self,&block) 103 | end 104 | end 105 | 106 | # 107 | # Runs a command on the destination server, in the destination 108 | # directory. 109 | # 110 | # @return [true] 111 | # 112 | # @since 0.3.0 113 | # 114 | def exec(command) 115 | remote_shell do |shell| 116 | shell.cd(shell.uri.path) 117 | shell.exec(command) 118 | end 119 | 120 | return true 121 | end 122 | 123 | # 124 | # Executes a Rake task on the destination server, in the destination 125 | # directory. 126 | # 127 | # @return [true] 128 | # 129 | # @since 0.3.0 130 | # 131 | def rake(task,*arguments) 132 | remote_shell do |shell| 133 | shell.cd(shell.uri.path) 134 | shell.rake(task,*arguments) 135 | end 136 | 137 | return true 138 | end 139 | 140 | # 141 | # Starts an SSH session with the destination server. 142 | # 143 | # @param [Array] arguments 144 | # Additional arguments to pass to SSH. 145 | # 146 | # @return [true] 147 | # 148 | # @since 0.3.0 149 | # 150 | def ssh(*arguments) 151 | each_dest do |dest| 152 | RemoteShell.new(dest).ssh(*arguments) 153 | end 154 | 155 | return true 156 | end 157 | 158 | # 159 | # Sets up the deployment repository for the project. 160 | # 161 | # @param [Shell] shell 162 | # The remote shell to execute commands through. 163 | # 164 | # @since 0.3.0 165 | # 166 | def setup(shell) 167 | shell.status "Cloning #{@source} ..." 168 | 169 | shell.run 'git', 'clone', '--depth', 1, @source, shell.uri.path 170 | 171 | shell.status "Cloned #{@source}." 172 | end 173 | 174 | # 175 | # Updates the deployed repository for the project. 176 | # 177 | # @param [Shell] shell 178 | # The remote shell to execute commands through. 179 | # 180 | # @since 0.3.0 181 | # 182 | def update(shell) 183 | shell.status "Updating ..." 184 | 185 | shell.run 'git', 'reset', '--hard', 'HEAD' 186 | shell.run 'git', 'pull', '-f' 187 | 188 | shell.status "Updated." 189 | end 190 | 191 | # 192 | # Installs any additional dependencies. 193 | # 194 | # @param [Shell] shell 195 | # The remote shell to execute commands through. 196 | # 197 | # @since 0.3.0 198 | # 199 | def install(shell) 200 | if @bundler 201 | shell.status "Bundling dependencies ..." 202 | 203 | shell.run 'bundle', 'install', '--deployment' 204 | 205 | shell.status "Dependencies bundled." 206 | end 207 | end 208 | 209 | # 210 | # Place-holder method. 211 | # 212 | # @param [Shell] shell 213 | # The remote shell to execute commands through. 214 | # 215 | # @since 0.3.0 216 | # 217 | def migrate(shell) 218 | end 219 | 220 | # 221 | # Place-holder method. 222 | # 223 | # @param [Shell] shell 224 | # The remote shell to execute commands through. 225 | # 226 | # @since 0.3.0 227 | # 228 | def server_config(shell) 229 | end 230 | 231 | # 232 | # Place-holder method. 233 | # 234 | # @param [Shell] shell 235 | # The remote shell to execute commands through. 236 | # 237 | # @since 0.3.0 238 | # 239 | def server_start(shell) 240 | end 241 | 242 | # 243 | # Place-holder method. 244 | # 245 | # @param [Shell] shell 246 | # The remote shell to execute commands through. 247 | # 248 | # @since 0.3.0 249 | # 250 | def server_stop(shell) 251 | end 252 | 253 | # 254 | # Place-holder method. 255 | # 256 | # @param [Shell] shell 257 | # The remote shell to execute commands through. 258 | # 259 | # @since 0.3.0 260 | # 261 | def server_restart(shell) 262 | end 263 | 264 | # 265 | # Place-holder method. 266 | # 267 | # @param [Shell] shell 268 | # The remote shell to execute commands through. 269 | # 270 | # @since 0.5.0 271 | # 272 | def config(shell) 273 | server_config(shell) 274 | end 275 | 276 | # 277 | # Place-holder method. 278 | # 279 | # @param [Shell] shell 280 | # The remote shell to execute commands through. 281 | # 282 | # @since 0.5.0 283 | # 284 | def start(shell) 285 | server_start(shell) 286 | end 287 | 288 | # 289 | # Place-holder method. 290 | # 291 | # @param [Shell] shell 292 | # The remote shell to execute commands through. 293 | # 294 | # @since 0.5.0 295 | # 296 | def stop(shell) 297 | server_stop(shell) 298 | end 299 | 300 | # 301 | # Place-holder method. 302 | # 303 | # @param [Shell] shell 304 | # The remote shell to execute commands through. 305 | # 306 | # @since 0.5.0 307 | # 308 | def restart(shell) 309 | server_restart(shell) 310 | end 311 | 312 | # 313 | # Invokes a task. 314 | # 315 | # @param [Symbol] task 316 | # The name of the task to run. 317 | # 318 | # @param [Shell] shell 319 | # The shell to run the task in. 320 | # 321 | # @raise [RuntimeError] 322 | # The task name was not known. 323 | # 324 | # @since 0.5.0 325 | # 326 | def invoke_task(task,shell) 327 | unless TASKS.include?(task) 328 | raise("invalid task: #{task}") 329 | end 330 | 331 | if @before.has_key?(task) 332 | @before[task].each { |command| shell.exec(command) } 333 | end 334 | 335 | send(task,shell) if respond_to?(task) 336 | 337 | if @after.has_key?(task) 338 | @after[task].each { |command| shell.exec(command) } 339 | end 340 | end 341 | 342 | # 343 | # Deploys the project. 344 | # 345 | # @param [Array] tasks 346 | # The tasks to run during the deployment. 347 | # 348 | # @return [true] 349 | # Indicates that the tasks were successfully completed. 350 | # 351 | # @since 0.4.0 352 | # 353 | def invoke(tasks) 354 | remote_shell do |shell| 355 | # setup the deployment repository 356 | invoke_task(:setup,shell) if tasks.include?(:setup) 357 | 358 | # cd into the deployment repository 359 | shell.cd(shell.uri.path) 360 | 361 | # update the deployment repository 362 | invoke_task(:update,shell) if tasks.include?(:update) 363 | 364 | # framework tasks 365 | invoke_task(:install,shell) if tasks.include?(:install) 366 | invoke_task(:migrate,shell) if tasks.include?(:migrate) 367 | 368 | # server tasks 369 | if tasks.include?(:config) 370 | invoke_task(:config,shell) 371 | elsif tasks.include?(:start) 372 | invoke_task(:start,shell) 373 | elsif tasks.include?(:stop) 374 | invoke_task(:stop,shell) 375 | elsif tasks.include?(:restart) 376 | invoke_task(:restart,shell) 377 | end 378 | end 379 | 380 | return true 381 | end 382 | 383 | # 384 | # Sets up the deployment repository for the project. 385 | # 386 | # @return [true] 387 | # Indicates that the tasks were successfully completed. 388 | # 389 | # @since 0.4.0 390 | # 391 | def setup! 392 | invoke [:setup] 393 | end 394 | 395 | # 396 | # Updates the deployed repository of the project. 397 | # 398 | # @return [true] 399 | # Indicates that the tasks were successfully completed. 400 | # 401 | # @since 0.4.0 402 | # 403 | def update! 404 | invoke [:update] 405 | end 406 | 407 | # 408 | # Installs the project on the destination server. 409 | # 410 | # @return [true] 411 | # Indicates that the tasks were successfully completed. 412 | # 413 | # @since 0.4.0 414 | # 415 | def install! 416 | invoke [:install] 417 | end 418 | 419 | # 420 | # Migrates the database used by the project. 421 | # 422 | # @return [true] 423 | # Indicates that the tasks were successfully completed. 424 | # 425 | # @since 0.4.0 426 | # 427 | def migrate! 428 | invoke [:migrate] 429 | end 430 | 431 | # 432 | # Configures the Web server to be ran on the destination server. 433 | # 434 | # @return [true] 435 | # Indicates that the tasks were successfully completed. 436 | # 437 | # @since 0.4.0 438 | # 439 | def config! 440 | invoke [:config] 441 | end 442 | 443 | # 444 | # Starts the Web server for the project. 445 | # 446 | # @return [true] 447 | # Indicates that the tasks were successfully completed. 448 | # 449 | # @since 0.4.0 450 | # 451 | def start! 452 | invoke [:start] 453 | end 454 | 455 | # 456 | # Stops the Web server for the project. 457 | # 458 | # @return [true] 459 | # Indicates that the tasks were successfully completed. 460 | # 461 | # @since 0.4.0 462 | # 463 | def stop! 464 | invoke [:stop] 465 | end 466 | 467 | # 468 | # Restarts the Web server for the project. 469 | # 470 | # @return [true] 471 | # Indicates that the tasks were successfully completed. 472 | # 473 | # @since 0.4.0 474 | # 475 | def restart! 476 | invoke [:restart] 477 | end 478 | 479 | # 480 | # Deploys a new project. 481 | # 482 | # @return [true] 483 | # Indicates that the tasks were successfully completed. 484 | # 485 | # @since 0.4.0 486 | # 487 | def deploy! 488 | invoke [:setup, :install, :migrate, :config, :start] 489 | end 490 | 491 | # 492 | # Redeploys a project. 493 | # 494 | # @return [true] 495 | # Indicates that the tasks were successfully completed. 496 | # 497 | # @since 0.4.0 498 | # 499 | def redeploy! 500 | invoke [:update, :install, :migrate, :restart] 501 | end 502 | 503 | protected 504 | 505 | # 506 | # Loads the framework configuration. 507 | # 508 | # @since 0.3.0 509 | # 510 | def load_framework! 511 | if @orm 512 | unless FRAMEWORKS.has_key?(@framework) 513 | raise(UnknownFramework,"Unknown framework #{@framework}",caller) 514 | end 515 | 516 | extend FRAMEWORKS[@framework] 517 | 518 | initialize_framework if respond_to?(:initialize_framework) 519 | end 520 | end 521 | 522 | # 523 | # Loads the server configuration. 524 | # 525 | # @raise [UnknownServer] 526 | # 527 | # @since 0.3.0 528 | # 529 | def load_server! 530 | if @server_name 531 | unless SERVERS.has_key?(@server_name) 532 | raise(UnknownServer,"Unknown server name #{@server_name}",caller) 533 | end 534 | 535 | extend SERVERS[@server_name] 536 | 537 | initialize_server if respond_to?(:initialize_server) 538 | end 539 | end 540 | 541 | end 542 | end 543 | --------------------------------------------------------------------------------