git-runner 0.0.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.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .rvmrc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in git-runner.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 James Brooks
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # GitRunner
2
+
3
+ GitRunner is a ruby framework to implement and run tasks after code has been pushed to a Git repository. It works by invoking `git-runner` through `hooks/post-update` in your remote Git repository.
4
+
5
+ Configuration for GitRunner is read from a pre-determined file within the repository (at this time, that file is config/deploy.rb but this will soon be configurable). Any instructions detected are then processed and ran.
6
+
7
+ Instructions are contained within `lib/git-runner/instructions`, though soon these will be extracted out to separate gems, leaving only the core instructions present. Currently the only *real* instruction that users will want to run is Deploy, which will checkout your code, run bundler and perform a cap deploy (with multistage support).
8
+
9
+ ## Installation
10
+
11
+ $ gem install git-runner
12
+
13
+ ## Usage
14
+
15
+ Symlink `hooks/post-update` to `git-runner`, or if `post-update` is already in use modify it to run `git-runner` with the arguments supplied to the hook.
16
+
17
+ ## TODO
18
+
19
+ * Allow file based configuration (currently configuration is hard-coded)
20
+ * Extract our non-core instruction functionality into individual gems (e.g. the Deploy instruction)
21
+
22
+ ## Contributing
23
+
24
+ 1. Fork it
25
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
26
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
27
+ 4. Push to the branch (`git push origin my-new-feature`)
28
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/bin/git-runner ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ require 'pathname'
5
+
6
+ $:.unshift File.expand_path('../../lib', Pathname.new(__FILE__).realpath)
7
+
8
+
9
+ require 'git-runner'
10
+
11
+ GitRunner::Base.new(ARGV).run
@@ -0,0 +1,18 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/git-runner/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["James Brooks"]
6
+ gem.email = ["james@jamesbrooks.net"]
7
+ gem.summary = "Ruby framework to run tasks after code has been pushed to a Git repository."
8
+ gem.description = "GitRunner is a ruby framework to implement and run tasks after code has been pushed to a Git repository. It works by invoking `git-runner` through `hooks/post-update` in your remote Git repository."
9
+ gem.homepage = "https://github.com/JamesBrooks/git-runner"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.name = "git-runner"
14
+ gem.require_paths = ["lib"]
15
+ gem.version = GitRunner::VERSION
16
+
17
+ gem.add_dependency 'session'
18
+ end
@@ -0,0 +1,61 @@
1
+ module GitRunner
2
+ class Base
3
+ attr_accessor :refs
4
+
5
+
6
+ def initialize(refs=[])
7
+ self.refs = refs
8
+ end
9
+
10
+ def run
11
+ begin
12
+ Text.begin
13
+
14
+ if refs && refs.is_a?(Array)
15
+ repository_path = Dir.pwd
16
+
17
+ # Only process HEAD references
18
+ refs.select { |str| str =~ /^refs\/heads\// }.each do |ref|
19
+ branch_name = ref.split('/').last
20
+ branch = Branch.new(repository_path, branch_name)
21
+
22
+ branch.run
23
+ end
24
+ end
25
+
26
+
27
+ rescue GitRunner::Instruction::InstructionHalted => ex
28
+ Text.out("Stopping runner, no further instructions will be performed\n")
29
+
30
+ rescue GitRunner::Command::Failure => ex
31
+ Text.out("\n")
32
+ Text.out(Text.red("\u2716 Command failed: " + Text.red(ex.result.command)), :heading)
33
+
34
+ # Write error log
35
+ log_directory = File.join(Configuration.tmp_directory, 'logs')
36
+ error_log = File.join(log_directory, Time.now.strftime("%Y%m%d%H%M%S") + '-error.log')
37
+
38
+ Dir.mkdir(log_directory) unless Dir.exists?(log_directory)
39
+ File.open(error_log, 'w') do |file|
40
+ Command.history.each do |result|
41
+ file << "Status: #{result.status}; Command: #{result.command}\n"
42
+ file << result.out
43
+ file << "\n--------------------\n"
44
+ end
45
+ end
46
+
47
+ Text.out("An error log has been created: #{error_log}")
48
+ Text.new_line
49
+
50
+ rescue Exception => ex
51
+ Text.new_line
52
+ Text.out(Text.red("\u2716 Unknown exception occured: #{ex}"), :heading)
53
+ Text.out(ex.backtrace.join("\n").gsub("\n", "\n#{''.ljust(10)}"))
54
+ Text.new_line
55
+
56
+ ensure
57
+ Text.finish
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,42 @@
1
+ module GitRunner
2
+ class Branch
3
+ attr_accessor :repository_path, :name
4
+
5
+
6
+ def initialize(repository_path, name)
7
+ self.repository_path = repository_path
8
+ self.name = name
9
+ end
10
+
11
+ def run
12
+ instructions.each do |instruction|
13
+ instruction.run
14
+ end
15
+ end
16
+
17
+ def instructions
18
+ @instructions ||= extract_instructions
19
+ end
20
+
21
+ def repository_name
22
+ repository_path.split(File::SEPARATOR).last.gsub('.git', '')
23
+ end
24
+
25
+ private
26
+ def extract_instructions
27
+ # Use git to grep the current branch for instruction lines within the specific instruction file
28
+ output = Command.execute(
29
+ "cd #{repository_path}",
30
+ "git grep '^#{Configuration.instruction_prefix}' #{name}:#{Configuration.instruction_file}"
31
+ )
32
+
33
+ # Process the output to generate instructions
34
+ output.split("\n").map do |line|
35
+ instruction = Instruction.from_raw(line)
36
+ instruction.branch = self
37
+
38
+ instruction
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,66 @@
1
+ require 'session'
2
+
3
+ module GitRunner
4
+ module Command
5
+ extend self
6
+
7
+
8
+ def execute(*commands)
9
+ commands.each do |command|
10
+ out = StringIO::new
11
+
12
+ session.execute command, :stdout => out, :stderr => out
13
+
14
+ result = Result.new(command, out.string, session.exit_status)
15
+ history << result
16
+
17
+ raise Failure.new(result) if result.failure?
18
+ end
19
+
20
+ history.last.out
21
+ end
22
+
23
+ def history
24
+ @history ||= []
25
+ end
26
+
27
+
28
+ private
29
+ def session
30
+ @session ||= Session::Bash::Login.new
31
+ end
32
+ end
33
+ end
34
+
35
+ module GitRunner
36
+ module Command
37
+ class Result
38
+ attr_accessor :command, :out, :status
39
+
40
+
41
+ def initialize(command, out, status)
42
+ self.command = command
43
+ self.out = out
44
+ self.status = status
45
+ end
46
+
47
+ def success?
48
+ status == 0
49
+ end
50
+
51
+ def failure?
52
+ !success?
53
+ end
54
+ end
55
+
56
+ class Failure < StandardError
57
+ attr_accessor :result
58
+
59
+
60
+ def initialize(result)
61
+ super(result.command)
62
+ self.result = result
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,20 @@
1
+ module GitRunner
2
+ module Configuration
3
+ extend self
4
+
5
+ def __attributes
6
+ # TODO: Load configuration from /etc/git-runner.conf
7
+ @attributes ||= DEFAULT_CONFIGURATION.dup
8
+ end
9
+
10
+ def method_missing(method, *args, &block)
11
+ if __attributes.keys.include?(method)
12
+ __attributes[method]
13
+ elsif method.to_s.end_with?('=')
14
+ __attributes[method.to_s[0..-2].to_sym] = args[0]
15
+ else
16
+ super
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ Dir[File.join(File.dirname(__FILE__), 'instructions', '*.rb')].each { |file| require file }
2
+
3
+ module GitRunner
4
+ class Instruction
5
+ class InstructionHalted < StandardError ; end
6
+
7
+
8
+ def self.new(name, args={})
9
+ begin
10
+ const_get(name).new(args)
11
+
12
+ rescue NameError => e
13
+ # Return a console message instruction to inform the user
14
+ instruction = ConsoleMessage.new(Text.red("\u2716 Instruction not found: #{name}"))
15
+ instruction.opts = {
16
+ :halt => true,
17
+ :priority => true
18
+ }
19
+
20
+ instruction
21
+ end
22
+ end
23
+
24
+ def self.from_raw(raw)
25
+ new(*%r[#{Regexp.escape(Configuration.instruction_prefix)}\s*(\w+)\s*(.*)$].match(raw).captures)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,50 @@
1
+ module GitRunner
2
+ class Instruction
3
+ class Base
4
+ attr_accessor :branch, :args, :opts
5
+
6
+
7
+ def initialize(args)
8
+ self.args = args
9
+ end
10
+
11
+ def perform
12
+ # No-op
13
+ end
14
+
15
+ def run
16
+ if should_run?
17
+ perform
18
+ Text.new_line
19
+
20
+ if halt?
21
+ raise InstructionHalted.new(self)
22
+ end
23
+ end
24
+ end
25
+
26
+ def should_run?
27
+ true
28
+ end
29
+
30
+ def halt!
31
+ self.opts ||= {}
32
+ self.opts[:halt] = true
33
+ end
34
+
35
+ def halt?
36
+ self.opts && !!self.opts[:halt]
37
+ end
38
+
39
+ def priority?
40
+ self.opts && !!self.opts[:priority]
41
+ end
42
+
43
+
44
+ private
45
+ def execute(*commands)
46
+ Command.execute(*commands)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,13 @@
1
+ module GitRunner
2
+ class Instruction
3
+ class ConsoleMessage < Base
4
+ def perform
5
+ Text.out(message, :heading)
6
+ end
7
+
8
+ def message
9
+ args
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,111 @@
1
+ require 'digest'
2
+
3
+ module GitRunner
4
+ class Instruction
5
+
6
+ # Performs deployments using capistrano (cap deploy)
7
+ class Deploy < Base
8
+ attr_accessor :clone_directory
9
+
10
+
11
+ def should_run?
12
+ branches.empty? || branches.include?(branch.name)
13
+ end
14
+
15
+ def perform
16
+ start_time = Time.now
17
+
18
+ Text.out(Text.green("Performing Deploy (#{environment_from_branch(branch)})"), :heading)
19
+
20
+ checkout_branch
21
+
22
+ if missing_capfile?
23
+ Text.out(Text.red("Missing Capfile, unable to complete deploy."))
24
+ halt! && return
25
+ end
26
+
27
+ prepare_deploy_environment
28
+ perform_deploy
29
+ cleanup_deploy_environment
30
+
31
+ end_time = Time.now
32
+
33
+ Text.out(Text.green("\u2714 Deploy successful, completed in #{(end_time - start_time).ceil} seconds"))
34
+ end
35
+
36
+ def branches
37
+ args.split(/\s+/)
38
+ end
39
+
40
+ def rvm_string
41
+ "#{Configuration.rvm_ruby_version}@git-runner-#{branch.repository_name}-#{branch.name}"
42
+ end
43
+
44
+ def missing_capfile?
45
+ !File.exists?("#{clone_directory}/Capfile")
46
+ end
47
+
48
+ def uses_bundler?
49
+ File.exists?("#{clone_directory}/Gemfile")
50
+ end
51
+
52
+ def multistage?
53
+ result = execute("grep -e 'require.*capistrano.*multistage' #{clone_directory}/#{Configuration.instruction_file} || true")
54
+ !result.empty?
55
+ end
56
+
57
+ def checkout_branch
58
+ timestamp = Time.now.strftime("%Y%m%d%H%M%S")
59
+ self.clone_directory = File.join(Configuration.tmp_directory, "#{branch.repository_name}-#{environment_from_branch(branch)}-#{timestamp}")
60
+
61
+ Text.out("Checking out #{branch.name} to #{clone_directory}")
62
+
63
+ execute(
64
+ "mkdir -p #{clone_directory}",
65
+ "git clone --depth=1 --branch=#{branch.name} file://#{branch.repository_path} #{clone_directory}"
66
+ )
67
+ end
68
+
69
+ def prepare_deploy_environment
70
+ Text.out("Preparing deploy environment")
71
+
72
+ if uses_bundler?
73
+ execute(
74
+ "cd #{clone_directory}",
75
+ "bundle install --path=#{File.join(Configuration.tmp_directory, '.gems')}"
76
+ )
77
+ end
78
+ end
79
+
80
+ def perform_deploy
81
+ cap_deploy_command = if multistage?
82
+ Text.out("Deploying application (multistage detected)")
83
+ "cap #{environment_from_branch(branch)} deploy"
84
+ else
85
+ Text.out("Deploying application")
86
+ "cap deploy"
87
+ end
88
+
89
+ execute(
90
+ "cd #{clone_directory}",
91
+ cap_deploy_command
92
+ )
93
+ end
94
+
95
+ def cleanup_deploy_environment
96
+ Text.out("Cleaning deploy environment")
97
+ execute("rm -rf #{clone_directory}")
98
+ end
99
+
100
+
101
+ private
102
+ def environment_from_branch(branch)
103
+ if branch.name == 'master'
104
+ 'production'
105
+ else
106
+ branch.name
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,67 @@
1
+ module GitRunner
2
+ class Text
3
+ COLOR_START = "\033["
4
+ COLOR_END = "m"
5
+ COLOR_RESET = "#{COLOR_START}0#{COLOR_END}"
6
+
7
+ COLORS = {
8
+ :black => 30,
9
+ :red => 31,
10
+ :green => 32,
11
+ :yellow => 33,
12
+ :blue => 34,
13
+ :purple => 35,
14
+ :cyan => 36,
15
+ :white => 37
16
+ }
17
+
18
+ STYLES = {
19
+ :normal => 0,
20
+ :bold => 1,
21
+ :underline => 4
22
+ }
23
+
24
+
25
+ class << self
26
+ def begin
27
+ STDOUT.sync = true
28
+ print("\e[1G \e[1G")
29
+ end
30
+
31
+ def finish
32
+ print("\n")
33
+ end
34
+
35
+ def out(str, style=nil)
36
+ # Ensure that new lines overwrite the default 'remote: ' text
37
+ str = str.gsub("\n", "\n\e[1G#{padding(nil)}")
38
+
39
+ print "\n\e[1G#{padding(style).ljust(7)}#{str}"
40
+ end
41
+
42
+ def new_line
43
+ out('')
44
+ end
45
+
46
+ # Color methods
47
+ COLORS.each do |color, code|
48
+ class_eval <<-EOF
49
+ def #{color}(str, style=:normal)
50
+ "#{COLOR_START}\#{STYLES[style]};#{code}#{COLOR_END}\#{str}#{COLOR_RESET}"
51
+ end
52
+ EOF
53
+ end
54
+
55
+
56
+ private
57
+ def padding(style)
58
+ case style
59
+ when :heading
60
+ '----->'
61
+ else
62
+ ''
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,3 @@
1
+ module GitRunner
2
+ VERSION = '0.0.1'
3
+ end
data/lib/git-runner.rb ADDED
@@ -0,0 +1,17 @@
1
+ require "git-runner/version"
2
+ require "git-runner/base"
3
+ require "git-runner/command"
4
+ require "git-runner/configuration"
5
+ require "git-runner/branch"
6
+ require "git-runner/instruction"
7
+ require "git-runner/text"
8
+
9
+ module GitRunner
10
+ DEFAULT_CONFIGURATION = {
11
+ :git_executable => '/usr/bin/env git',
12
+ :instruction_file => 'config/deploy.rb',
13
+ :instruction_prefix => '# GitRunner:',
14
+ :tmp_directory => '/tmp/git-runner',
15
+ :rvm_ruby_version => '1.9.3'
16
+ }
17
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: git-runner
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - James Brooks
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-05 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: session
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: GitRunner is a ruby framework to implement and run tasks after code has
31
+ been pushed to a Git repository. It works by invoking `git-runner` through `hooks/post-update`
32
+ in your remote Git repository.
33
+ email:
34
+ - james@jamesbrooks.net
35
+ executables:
36
+ - git-runner
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - .gitignore
41
+ - Gemfile
42
+ - LICENSE
43
+ - README.md
44
+ - Rakefile
45
+ - bin/git-runner
46
+ - git-runner.gemspec
47
+ - lib/git-runner.rb
48
+ - lib/git-runner/base.rb
49
+ - lib/git-runner/branch.rb
50
+ - lib/git-runner/command.rb
51
+ - lib/git-runner/configuration.rb
52
+ - lib/git-runner/instruction.rb
53
+ - lib/git-runner/instructions/base.rb
54
+ - lib/git-runner/instructions/console_message.rb
55
+ - lib/git-runner/instructions/deploy.rb
56
+ - lib/git-runner/text.rb
57
+ - lib/git-runner/version.rb
58
+ homepage: https://github.com/JamesBrooks/git-runner
59
+ licenses: []
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ! '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubyforge_project:
78
+ rubygems_version: 1.8.24
79
+ signing_key:
80
+ specification_version: 3
81
+ summary: Ruby framework to run tasks after code has been pushed to a Git repository.
82
+ test_files: []