hercules 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. data/.gitignore +10 -0
  2. data/Gemfile +11 -0
  3. data/Gemfile.lock +36 -0
  4. data/LICENSE +20 -0
  5. data/README.md +71 -0
  6. data/Rakefile +76 -0
  7. data/VERSION +1 -0
  8. data/bin/hercules +6 -0
  9. data/hdi/README.md +4 -0
  10. data/hdi/config.rb +7 -0
  11. data/hdi/site/index.html +267 -0
  12. data/hdi/site/stylesheets/style.css +1 -0
  13. data/hdi/src/configuration.rb +5 -0
  14. data/hdi/src/layouts/application.haml +12 -0
  15. data/hdi/src/pages/_hdi.haml +71 -0
  16. data/hdi/src/pages/_header.haml +8 -0
  17. data/hdi/src/pages/_jquery.haml +155 -0
  18. data/hdi/src/pages/index.haml +10 -0
  19. data/hdi/src/stylesheets/style.sass +173 -0
  20. data/hercules.gemspec +120 -0
  21. data/lib/command_runner.rb +73 -0
  22. data/lib/config.rb +82 -0
  23. data/lib/deployer.rb +95 -0
  24. data/lib/git_handler.rb +78 -0
  25. data/lib/hercules.rb +167 -0
  26. data/lib/http_handler.rb +41 -0
  27. data/lib/request_handler.rb +142 -0
  28. data/tests/command_runner_test.rb +35 -0
  29. data/tests/config_test.rb +39 -0
  30. data/tests/fixtures/Gemfile +1 -0
  31. data/tests/fixtures/Gemfile.lock +8 -0
  32. data/tests/fixtures/Gemfile.with_git_gem +2 -0
  33. data/tests/fixtures/Gemfile.with_git_gem.lock +10 -0
  34. data/tests/fixtures/bogus_config.yml +9 -0
  35. data/tests/fixtures/bogus_deployer.rb +2 -0
  36. data/tests/fixtures/config.yml +12 -0
  37. data/tests/fixtures/config_empty.yml +1 -0
  38. data/tests/fixtures/config_empty_branches.yml +8 -0
  39. data/tests/fixtures/config_empty_projects.yml +2 -0
  40. data/tests/fixtures/config_global.yml +14 -0
  41. data/tests/fixtures/config_partial_1.yml +7 -0
  42. data/tests/fixtures/config_partial_2.yml +7 -0
  43. data/tests/fixtures/config_partial_3.yml +7 -0
  44. data/tests/fixtures/deployer_branch.rb +11 -0
  45. data/tests/fixtures/deployer_exception.rb +11 -0
  46. data/tests/fixtures/deployer_false.rb +11 -0
  47. data/tests/fixtures/deployer_path.rb +11 -0
  48. data/tests/fixtures/deployer_true.rb +11 -0
  49. data/tests/fixtures/deployer_undefined_variable.rb +11 -0
  50. data/tests/fixtures/startup_checkout_config.yml +12 -0
  51. data/tests/fixtures/startup_checkout_error_config.yml +12 -0
  52. data/tests/git_handler_test.rb +95 -0
  53. data/tests/git_setup.rb +70 -0
  54. data/tests/hercules_test.rb +128 -0
  55. data/tests/http_handler_test.rb +88 -0
  56. data/tests/request_handler_test.rb +242 -0
  57. data/tests/startup.rb +36 -0
  58. metadata +251 -0
data/hercules.gemspec ADDED
@@ -0,0 +1,120 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{hercules}
8
+ s.version = "0.1.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Diogo Biazus"]
12
+ s.date = %q{2010-10-14}
13
+ s.default_executable = %q{hercules}
14
+ s.description = %q{Very simple deployment tool. It was made to deploy rails applications using github, bundler.}
15
+ s.email = %q{diogob@gmail.com}
16
+ s.executables = ["hercules"]
17
+ s.extra_rdoc_files = [
18
+ "LICENSE",
19
+ "README.md"
20
+ ]
21
+ s.files = [
22
+ ".gitignore",
23
+ "Gemfile",
24
+ "Gemfile.lock",
25
+ "LICENSE",
26
+ "README.md",
27
+ "Rakefile",
28
+ "VERSION",
29
+ "bin/hercules",
30
+ "hdi/README.md",
31
+ "hdi/config.rb",
32
+ "hdi/site/index.html",
33
+ "hdi/site/stylesheets/style.css",
34
+ "hdi/src/configuration.rb",
35
+ "hdi/src/layouts/application.haml",
36
+ "hdi/src/pages/_hdi.haml",
37
+ "hdi/src/pages/_header.haml",
38
+ "hdi/src/pages/_jquery.haml",
39
+ "hdi/src/pages/index.haml",
40
+ "hdi/src/stylesheets/style.sass",
41
+ "hercules.gemspec",
42
+ "lib/command_runner.rb",
43
+ "lib/config.rb",
44
+ "lib/deployer.rb",
45
+ "lib/git_handler.rb",
46
+ "lib/hercules.rb",
47
+ "lib/http_handler.rb",
48
+ "lib/request_handler.rb",
49
+ "tests/command_runner_test.rb",
50
+ "tests/config_test.rb",
51
+ "tests/fixtures/Gemfile",
52
+ "tests/fixtures/Gemfile.lock",
53
+ "tests/fixtures/Gemfile.with_git_gem",
54
+ "tests/fixtures/Gemfile.with_git_gem.lock",
55
+ "tests/fixtures/bogus_config.yml",
56
+ "tests/fixtures/bogus_deployer.rb",
57
+ "tests/fixtures/config.yml",
58
+ "tests/fixtures/config_empty.yml",
59
+ "tests/fixtures/config_empty_branches.yml",
60
+ "tests/fixtures/config_empty_projects.yml",
61
+ "tests/fixtures/config_global.yml",
62
+ "tests/fixtures/config_partial_1.yml",
63
+ "tests/fixtures/config_partial_2.yml",
64
+ "tests/fixtures/config_partial_3.yml",
65
+ "tests/fixtures/deployer_branch.rb",
66
+ "tests/fixtures/deployer_exception.rb",
67
+ "tests/fixtures/deployer_false.rb",
68
+ "tests/fixtures/deployer_path.rb",
69
+ "tests/fixtures/deployer_true.rb",
70
+ "tests/fixtures/deployer_undefined_variable.rb",
71
+ "tests/fixtures/startup_checkout_config.yml",
72
+ "tests/fixtures/startup_checkout_error_config.yml",
73
+ "tests/git_handler_test.rb",
74
+ "tests/git_setup.rb",
75
+ "tests/hercules_test.rb",
76
+ "tests/http_handler_test.rb",
77
+ "tests/request_handler_test.rb",
78
+ "tests/startup.rb"
79
+ ]
80
+ s.homepage = %q{http://github.com/diogob/hercules}
81
+ s.rdoc_options = ["--charset=UTF-8"]
82
+ s.require_paths = ["lib"]
83
+ s.rubygems_version = %q{1.3.7}
84
+ s.summary = %q{Simple deploy solution for ruby applications (using github+bundler).}
85
+
86
+ if s.respond_to? :specification_version then
87
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
88
+ s.specification_version = 3
89
+
90
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
91
+ s.add_runtime_dependency(%q<eventmachine>, ["= 0.12.10"])
92
+ s.add_runtime_dependency(%q<eventmachine_httpserver>, ["= 0.2.0"])
93
+ s.add_runtime_dependency(%q<git>, ["= 1.2.5"])
94
+ s.add_runtime_dependency(%q<json>, ["= 1.4.6"])
95
+ s.add_runtime_dependency(%q<bundler>, ["~> 1.0.0"])
96
+ s.add_development_dependency(%q<haml>, ["= 3.0.18"])
97
+ s.add_development_dependency(%q<compass>, ["= 0.10.5"])
98
+ s.add_development_dependency(%q<staticmatic>, ["= 0.10.8"])
99
+ else
100
+ s.add_dependency(%q<eventmachine>, ["= 0.12.10"])
101
+ s.add_dependency(%q<eventmachine_httpserver>, ["= 0.2.0"])
102
+ s.add_dependency(%q<git>, ["= 1.2.5"])
103
+ s.add_dependency(%q<json>, ["= 1.4.6"])
104
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
105
+ s.add_dependency(%q<haml>, ["= 3.0.18"])
106
+ s.add_dependency(%q<compass>, ["= 0.10.5"])
107
+ s.add_dependency(%q<staticmatic>, ["= 0.10.8"])
108
+ end
109
+ else
110
+ s.add_dependency(%q<eventmachine>, ["= 0.12.10"])
111
+ s.add_dependency(%q<eventmachine_httpserver>, ["= 0.2.0"])
112
+ s.add_dependency(%q<git>, ["= 1.2.5"])
113
+ s.add_dependency(%q<json>, ["= 1.4.6"])
114
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
115
+ s.add_dependency(%q<haml>, ["= 3.0.18"])
116
+ s.add_dependency(%q<compass>, ["= 0.10.5"])
117
+ s.add_dependency(%q<staticmatic>, ["= 0.10.8"])
118
+ end
119
+ end
120
+
@@ -0,0 +1,73 @@
1
+ module Hercules
2
+ # Class to run the shell commands and store their output.
3
+ # Yes, this class was kindly provided by Integrity.
4
+ # A very nice CI solution built with ruby: http://github.com/integrity/integrity
5
+ # I've made some modifications to store all the commands ran in an output log.
6
+ class CommandRunner
7
+ attr_reader :output
8
+
9
+ class Error < StandardError # :nodoc:
10
+ end
11
+
12
+ Result = Struct.new(:success, :output)
13
+
14
+ # We need to inform the logger object to initialize a CommandRunner
15
+ def initialize(logger)
16
+ @logger = logger
17
+ @output = ""
18
+ end
19
+
20
+ # This method will store the output of every command ran by this
21
+ # instance on a file.
22
+ # * path is the file path where we want to store the log.
23
+ def store_output path
24
+ File.open(path, 'a+'){|f| f.write @output }
25
+ end
26
+
27
+ # Change the working directory.
28
+ # * dir is the new working directory.
29
+ def cd(dir)
30
+ @dir = dir
31
+ self
32
+ end
33
+
34
+ # Run a command using IO.popen, append output to @output
35
+ # * command is the string containing the shell command that will be run.
36
+ def run(command)
37
+ cmd = normalize(command)
38
+
39
+ @logger.debug(cmd)
40
+
41
+ output = ""
42
+ IO.popen(cmd, "r") { |io| output = io.read }
43
+
44
+ @output += output
45
+ Result.new($?.success?, output.chomp)
46
+ end
47
+
48
+ # Run a command using IO.popen, append output to @output
49
+ # But we raise an error in case the command is not successful.
50
+ # * command is the string containing the shell command that will be run.
51
+ def run!(command)
52
+ result = run(command)
53
+
54
+ unless result.success
55
+ @logger.error(result.output.inspect)
56
+ raise Error, "Failed to run '#{command}'"
57
+ end
58
+ @logger.debug(result.output.inspect)
59
+
60
+ result
61
+ end
62
+
63
+ # We change the working directory befor executing anything.
64
+ # * cmd is the command to be executed.
65
+ def normalize(cmd)
66
+ if @dir
67
+ "(cd #{@dir} && #{cmd} 2>&1)"
68
+ else
69
+ "(#{cmd} 2>&1)"
70
+ end
71
+ end
72
+ end
73
+ end
data/lib/config.rb ADDED
@@ -0,0 +1,82 @@
1
+ # coding: utf-8
2
+ require 'yaml'
3
+
4
+ module Hercules
5
+ class InvalidConfig < Exception; end
6
+ class Config
7
+ def initialize(path)
8
+ @config = nil
9
+ @path = path
10
+ reload
11
+ validate
12
+ end
13
+
14
+ def reload
15
+ @config = YAML.load_file(@path)
16
+ end
17
+
18
+ def [](k)
19
+ @config[k]
20
+ end
21
+
22
+ def each
23
+ @config.each do |k,v|
24
+ yield(k,v)
25
+ end
26
+ end
27
+
28
+ def include?(k)
29
+ @config.include?(k)
30
+ end
31
+
32
+ def host
33
+ @config['host'] || "0.0.0.0"
34
+ end
35
+
36
+ def port
37
+ @config['port'] || 8080
38
+ end
39
+
40
+ def projects
41
+ p = {}
42
+ @config.each do |k,v|
43
+ p[k] = v unless self.class.global_attributes.include?(k)
44
+ end
45
+ p
46
+ end
47
+
48
+ def self.global_attributes
49
+ ['host', 'port']
50
+ end
51
+
52
+ def self.project_attributes
53
+ ['target_directory', 'repository', 'token']
54
+ end
55
+
56
+ def self.branch_attributes
57
+ ['checkout_on_startup', 'checkouts_to_keep']
58
+ end
59
+
60
+ def branches
61
+ r = {}
62
+ projects.each do |k,v|
63
+ r[k] = v.keys.find_all{|e| e unless self.class.project_attributes.include?(e)}
64
+ end
65
+ r
66
+ end
67
+
68
+ private
69
+ def validate
70
+ # We need to test projects.empty? to cover cases where the config has only global attributes set.
71
+ raise InvalidConfig.new("Empty config file.") if @config.nil? or projects.empty?
72
+ projects.each do |k,v|
73
+ raise InvalidConfig.new("Config file error. #{k} expects a hash of options but got #{v}") unless v.is_a?(Hash)
74
+ # Every project attribute is mandatory
75
+ raise InvalidConfig.new("Config file lacks some project attribute, every project must have #{self.class.project_attributes.inspect}") unless !v.nil? and self.class.project_attributes & v.keys == self.class.project_attributes
76
+ branches[k].each do |branch|
77
+ raise InvalidConfig.new("Branch #{branch} in project #{k} lacks some branch attribute, every branch must have #{self.class.branch_attributes.inspect}") unless !@config[k][branch].nil? and self.class.branch_attributes & @config[k][branch].keys == self.class.branch_attributes
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
data/lib/deployer.rb ADDED
@@ -0,0 +1,95 @@
1
+ # coding: utf-8
2
+
3
+ require File.dirname(__FILE__) + '/git_handler'
4
+ require File.dirname(__FILE__) + '/command_runner'
5
+ require 'bundler'
6
+
7
+ module Hercules
8
+ # The Deployer is responsible for clonning the repository,
9
+ # copying the code, and calling the deploy triggers.
10
+ class Deployer
11
+ # * logger is the object of Logger class that will log the deploy actions.
12
+ # * config is the hash configuration of the project we want to deploy.
13
+ # Will be a subtree of the configuration YAML
14
+ # * branch is the name of the branch we are deploying.
15
+ def initialize(logger, config, branch)
16
+ @log = logger
17
+ @config = config
18
+ @branch = branch
19
+ @cmd = CommandRunner.new(@log)
20
+ @trigger_class = nil
21
+ end
22
+
23
+
24
+ # This method will do the deploy: git clone, run bundle install and call the triggers callbacks.
25
+ def deploy
26
+ git = GitHandler.new @config
27
+ git.deploy_branch(@branch) do |dir, branch|
28
+ Bundler.with_clean_env do
29
+ @cmd.cd(dir)
30
+ bundle_path = "#{dir}/../../../bundles/#{@branch}"
31
+ FileUtils.mkdir_p(bundle_path)
32
+ FileUtils.mkdir_p("#{dir}/vendor")
33
+ FileUtils.ln_s(bundle_path, "#{dir}/vendor/bundle", :force => true)
34
+ @cmd.run!("bundle install --deployment")
35
+ @trigger_class = look_for_triggers(dir)
36
+ before_trigger(dir) if has_before_trigger?
37
+ end
38
+ end
39
+ @log.warn "Branch #{@branch} deployed"
40
+ dir = "#{git.branches_path}/#{@branch}"
41
+ Bundler.with_clean_env do
42
+ after_trigger(dir) if has_after_trigger?
43
+ end
44
+ ensure
45
+ # Now we must store the deploy output
46
+ output_dir = "#{@config['target_directory']}/logs/#{@branch}/"
47
+ FileUtils.mkdir_p output_dir
48
+ @cmd.store_output "#{output_dir}/#{git.last_commit}.log"
49
+ end
50
+
51
+ private
52
+ # This method will execute the before trigger
53
+ # * dir is the working directory for trigger execution.
54
+ def before_trigger(dir)
55
+ @log.debug "Executing before_trigger"
56
+ Dir.chdir(dir) do
57
+ raise "before_deploy returned false." unless @trigger_class.before_deploy({:path => dir, :branch => @branch, :shell => @cmd})
58
+ end
59
+ end
60
+
61
+ # This method will execute the after trigger
62
+ # * dir is the working directory for trigger execution.
63
+ def after_trigger(dir)
64
+ @log.debug "Executing after_trigger"
65
+ Dir.chdir(dir) do
66
+ @trigger_class.after_deploy({:path => dir, :branch => @branch, :shell => @cmd})
67
+ end
68
+ @log.info "After deploy script executed"
69
+ end
70
+
71
+ # Check if the project has a before trigger
72
+ def has_before_trigger?; (!@trigger_class.nil? and @trigger_class.methods.include?("before_deploy")) ;end
73
+ # Check if the project has an after trigger
74
+ def has_after_trigger?; (!@trigger_class.nil? and @trigger_class.methods.include?("after_deploy")) ;end
75
+
76
+ # Look for triggers in <dir>/lib/hercules_triggers.rb
77
+ # The triggers should be inside a Hercules module in the Triggers class.
78
+ # * dir is the root dir where we will look for the triggers.
79
+ def look_for_triggers(dir)
80
+ if File.exists? "#{dir}/lib/hercules_triggers.rb"
81
+ require "#{dir}/lib/hercules_triggers.rb"
82
+ begin
83
+ @log.info "Looking for trigger in #{dir}/lib/hercules_triggers.rb"
84
+ return ::Hercules::Triggers
85
+ rescue NameError => e
86
+ # We have to allow the use of a lib/hercules_triggers.rb unrelated to Hercules
87
+ if e.message =~ /uninitialized constant .*Hercules.*/
88
+ @log.warn "File lib/deployer.rb without Hercules::Triggers: #{e.inspect}"
89
+ return nil
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,78 @@
1
+ # coding: utf-8
2
+ require 'git'
3
+
4
+ module Hercules
5
+ # Class that handles the git operations.
6
+ class GitHandler
7
+ attr_reader :last_commit
8
+ # We pass an options hash that should contain:
9
+ # {
10
+ # 'target_directory' => '/home/hercules/hercules.com',
11
+ # 'repository' => 'git://github.com/diogob/hercules.git',
12
+ # 'master' => { 'checkouts_to_keep' => 2 },
13
+ # }
14
+ def initialize(options)
15
+ @options = options
16
+ @last_commit = nil
17
+ end
18
+
19
+ # Will export the branch to @options['target_directory']/checkouts/
20
+ # And link it in @options['target_directory']/branches/
21
+ # It uses the commit's sha1 as directory name.
22
+ # * branch is the branch to be deployed, defaults to master.
23
+ def export_branch(branch = 'master')
24
+ tmp_dir = "#{@options['target_directory']}/checkouts/#{branch}/.tmp_#{Time.now.strftime("%Y%m%d%H%M%S")}"
25
+ begin
26
+ repo = Git.clone(@options['repository'], tmp_dir, {:depth => 1})
27
+ repo.checkout("origin/#{branch}")
28
+ rescue Exception => e
29
+ FileUtils.rm_rf repo.dir.to_s unless repo.nil?
30
+ raise "Error while cloning #{@options['repository']}: #{e}"
31
+ end
32
+ @last_commit = repo.gcommit('HEAD').sha
33
+ commit_dir = "#{@options['target_directory']}/checkouts/#{branch}/#{@last_commit}"
34
+ Dir.chdir(repo.dir.to_s) { FileUtils.rm_r '.git' }
35
+ FileUtils.mv repo.dir.to_s, commit_dir
36
+ commit_dir
37
+ end
38
+
39
+ # Returns the path to branches' link directory.
40
+ def branches_path
41
+ "#{@options['target_directory']}/branches"
42
+ end
43
+
44
+ # Creates and then returns the path to branches' link directory.
45
+ def create_branches_dir
46
+ FileUtils.mkdir_p branches_path
47
+ branches_path
48
+ end
49
+
50
+ # Deploys the branch.
51
+ # This means it exports it and removes old checkouts upon a successful completion.
52
+ # It also creates the links' directory and links the checkout.
53
+ # * branch is the branch name to be deployed. Defaults to master.
54
+ def deploy_branch(branch = 'master')
55
+ checkout = export_branch(branch)
56
+ #@todo here we must call the before deploy script
57
+ yield(checkout, branch) if block_given?
58
+ remove_old_checkouts branch
59
+ FileUtils.rm_f("#{create_branches_dir}/#{branch}")
60
+ FileUtils.ln_sf(checkout, "#{branches_path}/#{branch}")
61
+ self
62
+ end
63
+
64
+ private
65
+ def remove_old_checkouts(branch) # :nodoc:
66
+ max = @options[branch]['checkouts_to_keep']
67
+ dir = "#{@options['target_directory']}/checkouts/#{branch}"
68
+ if (Dir.glob("#{dir}/*").size) > max
69
+ # Here we must delete the oldest checkout
70
+ checkout_to_delete = Dir.glob("#{dir}/*").sort{|a,b| File.new(a).mtime.strftime("%Y%m%d%H%M%S") <=> File.new(b).mtime.strftime("%Y%m%d%H%M%S") }.shift
71
+ FileUtils.rm_r "#{checkout_to_delete}"
72
+ # Remove log file if it exists
73
+ # To achieve consistency we must remove the log when and only when we remove the checkout
74
+ FileUtils.rm_f "#{@options['target_directory']}/logs/#{checkout_to_delete.split('/').pop}.log"
75
+ end
76
+ end
77
+ end
78
+ end