octopusci 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/README.markdown +132 -0
  2. data/bin/octopusci-reset-redis +26 -0
  3. data/bin/octopusci-skel +2 -7
  4. data/bin/octopusci-tentacles +2 -2
  5. data/config.ru +1 -1
  6. data/lib/octopusci.rb +3 -7
  7. data/lib/octopusci/config.rb +63 -49
  8. data/lib/octopusci/errors.rb +2 -0
  9. data/lib/octopusci/helpers.rb +16 -15
  10. data/lib/octopusci/io.rb +70 -0
  11. data/lib/octopusci/job.rb +145 -34
  12. data/lib/octopusci/job_store.rb +67 -0
  13. data/lib/octopusci/notifier.rb +7 -17
  14. data/lib/octopusci/notifier/job_complete.html.erb +76 -3
  15. data/lib/octopusci/queue.rb +14 -10
  16. data/lib/octopusci/server.rb +17 -20
  17. data/lib/octopusci/server/views/index.erb +3 -4
  18. data/lib/octopusci/server/views/job.erb +3 -3
  19. data/lib/octopusci/server/views/job_summary.erb +18 -18
  20. data/lib/octopusci/server/views/layout.erb +6 -5
  21. data/lib/octopusci/stage_locker.rb +11 -7
  22. data/lib/octopusci/version.rb +1 -1
  23. data/lib/octopusci/worker_launcher.rb +1 -1
  24. data/spec/lib/octopusci/config_spec.rb +195 -0
  25. data/spec/lib/octopusci/io_spec.rb +64 -0
  26. data/spec/lib/octopusci/job_spec.rb +122 -0
  27. data/spec/lib/octopusci/job_store_spec.rb +155 -0
  28. data/spec/lib/octopusci/notifier_spec.rb +0 -15
  29. data/spec/lib/octopusci/queue_spec.rb +122 -0
  30. data/spec/lib/octopusci/server_spec.rb +92 -1
  31. data/spec/lib/octopusci/stage_locker_spec.rb +94 -0
  32. data/spec/spec_helper.rb +8 -0
  33. metadata +39 -58
  34. data/README +0 -63
  35. data/bin/octopusci-db-migrate +0 -10
  36. data/db/migrate/0001_init.rb +0 -29
  37. data/db/migrate/0002_add_status_job.rb +0 -19
  38. data/lib/octopusci/notifier/job_complete.text.erb +0 -5
  39. data/lib/octopusci/schema.rb +0 -140
@@ -0,0 +1,132 @@
1
+ Octopusci
2
+ =========
3
+
4
+ Octopusci is fresh new take on a continuous integration server centralized
5
+ around the concept of getting the same great CI benefits when using a
6
+ multi-branch workflow.
7
+
8
+ How's it Different?
9
+ -------------------
10
+
11
+ The impetus that brought Octopusci into being was simply the lack of CI servers
12
+ that cleanly supported a software development workflow based on multiple
13
+ branches. Secondarily, it was the excessive amount of effort necessary to get a
14
+ basic CI server up and running.
15
+
16
+ Octopsuci fills this gap by providing intelligent multi-branch queueing
17
+ and multi-server job distribution. Beyond that it provides a
18
+ solid continous integration server that is trivial to get setup and running. A number
19
+ of the concepts used in Octopusci are pulled from
20
+ [Continuous Delivery](http://continuousdelivery.com/),
21
+ [Continuous Integration](http://martinfowler.com/articles/continuousIntegration.html)
22
+ as well as Scott Chacon's post on the
23
+ [GitHub Flow](http://scottchacon.com/2011/08/31/github-flow.html).
24
+
25
+ The following is a listing of a number of some of its more significant features.
26
+
27
+ ### Dynamic Multi-Branch Triggering
28
+
29
+ Octopusci detects branch creation/modification and dynamically generates a build for
30
+ that branch based on the project the pushed branch belongs to. Most existing CI servers
31
+ that I have used force you to manually define jobs for each branch you would like it to
32
+ manage.
33
+
34
+ ### Multi-Server Job Distribution
35
+
36
+ Octopusci allows you to configure it to run "remote jobs" on numerous servers and it
37
+ keeps track of which servers are currently busy as well as handing new jobs to the
38
+ correct servers as they become available. This is extremely valuable if you are
39
+ interested in running automated acceptance tests that take a long time to run such
40
+ as Selenium/Cucumber & Capybara Tests.
41
+
42
+ ### Intelligent Multi-Branch Queueing
43
+
44
+ Octopusci intelligently manages its job queue by by simply updating any pending jobs with
45
+ the newly pushed branch data. This means that at any given point in time there is only
46
+ ever one pending job for each branch. When, a code push comes into Octopusci it
47
+ first looks to see if there is already a pending job for the branch that was pushed. If
48
+ there is, it simply updates the jobs associated branch data. If there is not already a
49
+ pending job then it queues a new job for that branch.
50
+
51
+ ### GitHub Integration ###
52
+
53
+ Octopusci was designed specifically to integrate cleanly with GitHub's push notifications
54
+ system. At some point in the future Octopusci may support more than just GitHub but for
55
+ the time being GitHub is our primary focus.
56
+
57
+ Install Guide
58
+ -------------
59
+
60
+ ### Install Dependencies ###
61
+
62
+ Octopusci has one major dependency at the moment, [Redis](http://redis.io/).
63
+ [Redis](http://redis.io/) needs to be installed and configured to startup appropriately
64
+ on the box you plan to run Octopusci on.
65
+
66
+ On Debian/Ubuntu machines this is to my knowledge as easy as `apt-get install redis-server`.
67
+
68
+ On Mac OS X machines this can easly be installed via [brew](http://mxcl.github.com/homebrew/)
69
+ using `brew install redis`. Follow the on screen instructions to configure it to auto
70
+ startup when you boot up as well as simply how to run the server manually.
71
+
72
+ ### Gem & Init Skel ###
73
+
74
+ $ gem install octopusci
75
+ $ sudo octopusci-skel
76
+
77
+ The `octopusci-skel` command will make sure the `/etc/octopusci` path exists and its
78
+ underlying structure. It will also create a default example `/etc/ocotpusci/config.yml`
79
+ if one is not found.
80
+
81
+ ### Update Example Config ###
82
+
83
+ Now that the `/etc/octopusci/config.yml` example config has been created for you it is
84
+ time to go check it out and update some of the values in it.
85
+
86
+ TODO: Fill this out with details on the config, required fields, optional fields, etc.
87
+
88
+ ### Jobs ###
89
+
90
+ Add any jobs you would like to the `/etc/octopusci/jobs` directory as .rb files
91
+ and Octopusci will load them appropriately when started.
92
+
93
+ ### Web Interface ###
94
+
95
+ Figure out what directory the gem is installed in by running the following
96
+ command and stripping off the `lib/octopusci.rb` at the end.
97
+
98
+ gem which octopusci
99
+
100
+ Once you have the path we can use that path to setup Passenger with Apache
101
+ or something else like nginx.
102
+
103
+ Apache virtual host example
104
+
105
+ <VirtualHost *:80>
106
+ ServerName octopusci.example.com
107
+ PassengerAppRoot /path/of/octpusci/we/got/before
108
+ DocumentRoot /path/of/octpusci/we/got/before/lib/octopusci/server/public
109
+ <Directory /path/of/octpusci/we/got/before/lib/octopusci/server/public>
110
+ Order allow,deny
111
+ Allow from all
112
+ AllowOverride all
113
+ Options -MultiViews
114
+ </Directory>
115
+ </VirtualHost>
116
+
117
+ The above will give us the web Octopusci web interface.
118
+
119
+ If you are developing you can simply start this up by running
120
+ `rackup -p whatever_port` while inside the octopusci directory where the
121
+ `config.ru` file exists.
122
+
123
+ I recommend you setup the second half of Octopusci (`octopusci-tentacles`) with
124
+ God or some other monitoring system. However, for development you can simply
125
+ run `octopusci-tentacles` directoly as follows:
126
+
127
+ otopusci-tentacles
128
+
129
+ Screenshots
130
+ -----------
131
+
132
+ ![Octopusci - Dashboard](https://img.skitch.com/20111005-tfxgw59mec5msnfu3pd6is3btf.jpg)
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+
5
+ require 'rubygems'
6
+ require 'octopusci'
7
+
8
+ num_jobs = Octopusci::JobStore.size
9
+ jobs = Octopusci::JobStore.list(0, num_jobs)
10
+
11
+ jobs.each do |j|
12
+ # delete the actual job record
13
+ Octopusci::JobStore.redis.del("octopusci:jobs:#{j['id']}")
14
+
15
+ # delete the repo_name, branch_name job references
16
+ Octopusci::JobStore.redis.del("octopusci:#{j['repo_name']}:#{j['branch_name']}:jobs")
17
+
18
+ # delete the github payload for the repo_name, branch_name
19
+ Octopusci::JobStore.redis.del("octopusci:github_payload:#{j['repo_name']}:#{j['branch_name']}")
20
+
21
+ end
22
+
23
+ Octopusci::JobStore.redis.del('octopusci:job_count')
24
+ Octopusci::JobStore.redis.del('octopusci:jobs')
25
+ Octopusci::JobStore.redis.del('octopusci:stagelocker')
26
+ Octopusci::JobStore.redis.del('queue:octopusci:commit')
@@ -19,6 +19,8 @@ if !File.exists?(CONFIG_PATH)
19
19
  File.open(CONFIG_PATH, 'w') do |f|
20
20
  f << "general:
21
21
  jobs_path: \"/etc/octopusci/jobs\"
22
+ base_url: \"http://localhost:9998\"
23
+ workspace_base_path: \"/Users/octopusci/.octopusci\"
22
24
 
23
25
  http_basic:
24
26
  username: admin
@@ -34,13 +36,6 @@ smtp:
34
36
  password: somepassword
35
37
  raise_delivery_errors: true
36
38
 
37
- db:
38
- adapter: mysql
39
- host: localhost
40
- database: octopusci
41
- username: someusername
42
- password: somepassword
43
-
44
39
  projects:
45
40
  - { name: octopusci, owner: cyphactor, job_klass: SomeJobClass, repo_uri: 'git@github.com:cyphactor/octopusci.git', default_email: devs@example.com }
46
41
 
@@ -26,8 +26,8 @@ require 'octopusci'
26
26
  # valid initial state. But, also in the case that octopusci-tentacles was
27
27
  # killed it nukes the potentially screwed up state the redis data is in and
28
28
  # initializes it the proper state based on the config.
29
- if Octopusci::CONFIG.has_key?('stages')
30
- Octopusci::StageLocker.load(Octopusci::CONFIG['stages'])
29
+ if Octopusci::Config.has_key?('stages')
30
+ Octopusci::StageLocker.load(Octopusci::Config['stages'])
31
31
  end
32
32
 
33
33
  case ARGV[0]
data/config.ru CHANGED
@@ -4,7 +4,7 @@ require 'octopusci/server'
4
4
 
5
5
  # Set the AUTH env variable to your basic auth password to protect Resque.
6
6
  Resque::Server.use Rack::Auth::Basic do |username, password|
7
- (username == Octopusci::CONFIG['http_basic']['username']) && (password == Octopusci::CONFIG['http_basic']['password'])
7
+ (username == Octopusci::Config['http_basic']['username']) && (password == Octopusci::Config['http_basic']['password'])
8
8
  end
9
9
 
10
10
  run Rack::URLMap.new("/" => Octopusci::Server.new, "/resque" => Resque::Server.new)
@@ -1,17 +1,13 @@
1
1
  require 'time-ago-in-words'
2
2
 
3
3
  require 'octopusci/version'
4
+ require 'octopusci/errors'
4
5
  require 'octopusci/helpers'
5
- require 'octopusci/schema'
6
+ require 'octopusci/io'
7
+ require 'octopusci/job_store'
6
8
  require 'octopusci/notifier'
7
9
  require 'octopusci/queue'
8
10
  require 'octopusci/stage_locker'
9
11
  require 'octopusci/job'
10
12
  require 'octopusci/config'
11
13
  require 'octopusci/worker_launcher'
12
-
13
- module Octopusci
14
- def self.greet
15
- return "Hello RSpec!"
16
- end
17
- end
@@ -1,25 +1,34 @@
1
1
  require 'octopusci/notifier'
2
- require 'active_record'
3
2
 
4
3
  module Octopusci
5
- class Config
4
+ class ConfigStore
6
5
  class MissingConfigField < RuntimeError; end
7
- class ConfigNotInitialized < RuntimeError; end
8
6
 
9
- def initialize()
7
+ def initialize
8
+ reset()
9
+ end
10
+
11
+ def reset
10
12
  @options = {}
11
13
  end
12
-
13
- # read the configuration values into the object from a YML file.
14
- def load_yaml(yaml_file)
15
- @options = YAML.load_file(yaml_file)
14
+
15
+ def options
16
+ @options
17
+ end
18
+
19
+ def load(yaml_file = nil, &block)
20
+ load_yaml(yaml_file) if !yaml_file.nil?
21
+ yield self if block
22
+ after_load()
23
+ end
24
+
25
+ def reload(yaml_file = nil, &block)
26
+ reset()
27
+ load(yaml_file, &block)
16
28
  end
17
29
 
18
30
  # allow options to be accessed as if this object is a Hash.
19
31
  def [](key_name)
20
- if @options.nil?
21
- raise ConfigNotInitialized, "Can't access the '#{key_name}' field because the config hasn't been initialized."
22
- end
23
32
  if !@options.has_key?(key_name.to_s())
24
33
  raise MissingConfigField, "'#{key_name}' is NOT defined as a config field."
25
34
  end
@@ -46,50 +55,55 @@ module Octopusci
46
55
  return self[key_name_str]
47
56
  end
48
57
  end
49
- end
50
-
51
- # On evaluation of the module it defines a new singleton of Config.
52
- if (!defined?(CONFIG))
53
- CONFIG = Config.new()
54
- end
55
58
 
56
- def self.configure(yaml_file = nil, &block)
57
- CONFIG.load_yaml(yaml_file) if !yaml_file.nil?
58
- yield CONFIG if block
59
+ def after_load(&block)
60
+ if block
61
+ @after_load = block
62
+ elsif @after_load
63
+ @after_load.call
64
+ end
65
+ end
66
+
67
+ private
59
68
 
60
- Notifier.default :from => Octopusci::CONFIG['smtp']['notification_from_email']
61
- Notifier.delivery_method = :smtp
62
- Notifier.smtp_settings = {
63
- :address => Octopusci::CONFIG['smtp']['address'],
64
- :port => Octopusci::CONFIG['smtp']['port'].to_s,
65
- :authentication => Octopusci::CONFIG['smtp']['authentication'],
66
- :enable_starttls_auto => Octopusci::CONFIG['smtp']['enable_starttls_auto'],
67
- :user_name => Octopusci::CONFIG['smtp']['user_name'],
68
- :password => Octopusci::CONFIG['smtp']['password'],
69
- :raise_delivery_errors => Octopusci::CONFIG['smtp']['raise_delivery_errors']
70
- }
71
- Notifier.logger = Logger.new(STDOUT)
69
+ # read the configuration values into the object from a YML file.
70
+ def load_yaml(yaml_file)
71
+ @options.merge!(YAML.load_file(yaml_file))
72
+ end
72
73
  end
73
- end
74
-
75
- # Load the actual config file
76
- Octopusci.configure("/etc/octopusci/config.yml")
77
74
 
78
- if Octopusci::CONFIG['stages'] == nil
79
- raise "You have defined stages as an option but have no items in it."
75
+ # On evaluation of the module it defines a new singleton of Config.
76
+ if (!defined?(::Octopusci::Config))
77
+ ::Octopusci::Config = ConfigStore.new()
78
+ end
80
79
  end
81
80
 
82
- ActiveRecord::Base.establish_connection(
83
- :adapter => Octopusci::CONFIG['db']['adapter'],
84
- :host => Octopusci::CONFIG['db']['host'],
85
- :database => Octopusci::CONFIG['db']['database'],
86
- :username => Octopusci::CONFIG['db']['username'],
87
- :password => Octopusci::CONFIG['db']['password']
88
- )
81
+ # Setup the config after load callback
82
+ Octopusci::Config.after_load do
83
+ if Octopusci::Config['stages'] == nil
84
+ raise "You have defined stages as an option but have no items in it."
85
+ end
86
+
87
+ Octopusci::Notifier.default :from => Octopusci::Config['smtp']['notification_from_email']
88
+ Octopusci::Notifier.delivery_method = :smtp
89
+ Octopusci::Notifier.smtp_settings = {
90
+ :address => Octopusci::Config['smtp']['address'],
91
+ :port => Octopusci::Config['smtp']['port'].to_s,
92
+ :authentication => Octopusci::Config['smtp']['authentication'],
93
+ :enable_starttls_auto => Octopusci::Config['smtp']['enable_starttls_auto'],
94
+ :user_name => Octopusci::Config['smtp']['user_name'],
95
+ :password => Octopusci::Config['smtp']['password'],
96
+ :raise_delivery_errors => Octopusci::Config['smtp']['raise_delivery_errors']
97
+ }
98
+ Octopusci::Notifier.logger = Logger.new(STDOUT)
89
99
 
90
- Dir.open(Octopusci::CONFIG['general']['jobs_path']) do |d|
91
- job_file_names = d.entries.reject { |e| e == '..' || e == '.' }
92
- job_file_names.each do |f_name|
93
- require Octopusci::CONFIG['general']['jobs_path'] + "/#{f_name}"
100
+ Dir.open(Octopusci::Config['general']['jobs_path']) do |d|
101
+ job_file_names = d.entries.reject { |e| e == '..' || e == '.' }
102
+ job_file_names.each do |f_name|
103
+ load Octopusci::Config['general']['jobs_path'] + "/#{f_name}"
104
+ end
94
105
  end
95
106
  end
107
+
108
+ # Load the actual config file
109
+ Octopusci::Config.load("/etc/octopusci/config.yml")
@@ -1,4 +1,6 @@
1
1
  module Octopusci
2
2
  # Raised when a method that is meant to be pure virtual that is not overloaded is called
3
3
  class PureVirtualMethod < RuntimeError; end
4
+ class JobRunFailed < RuntimeError; end
5
+ class JobHalted < RuntimeError; end
4
6
  end
@@ -7,31 +7,32 @@ module Octopusci
7
7
  attrs = {}
8
8
 
9
9
  # ref
10
- attrs[:ref] = gh_pl["ref"]
10
+ attrs['ref'] = gh_pl["ref"]
11
+ attrs['branch_name'] = gh_pl["ref"].split('/').last
11
12
  # compare
12
- attrs[:compare] = gh_pl["compare"]
13
+ attrs['compare'] = gh_pl["compare"]
13
14
  # repo_name
14
- attrs[:repo_name] = gh_pl["repository"]["name"]
15
+ attrs['repo_name'] = gh_pl["repository"]["name"]
15
16
  # repo_owner_name
16
- attrs[:repo_owner_name] = gh_pl["repository"]["owner"]["name"]
17
+ attrs['repo_owner_name'] = gh_pl["repository"]["owner"]["name"]
17
18
  # repo_owner_email
18
- attrs[:repo_owner_email] = gh_pl["repository"]["owner"]["email"]
19
+ attrs['repo_owner_email'] = gh_pl["repository"]["owner"]["email"]
19
20
  # repo_pushed_at
20
- attrs[:repo_pushed_at] = gh_pl["repository"]["pushed_at"]
21
+ attrs['repo_pushed_at'] = gh_pl["repository"]["pushed_at"]
21
22
  # repo_created_at
22
- attrs[:repo_created_at] = gh_pl["repository"]["created_at"]
23
+ attrs['repo_created_at'] = gh_pl["repository"]["created_at"]
23
24
  # repo_desc
24
- attrs[:repo_desc] = gh_pl["repository"]["description"]
25
+ attrs['repo_desc'] = gh_pl["repository"]["description"]
25
26
  # repo_url
26
- attrs[:repo_url] = gh_pl["repository"]["url"]
27
+ attrs['repo_url'] = gh_pl["repository"]["url"]
27
28
  # before_commit
28
- attrs[:before_commit] = gh_pl["before"]
29
+ attrs['before_commit'] = gh_pl["before"]
29
30
  # forced
30
- attrs[:forced] = gh_pl["forced"]
31
+ attrs['forced'] = gh_pl["forced"]
31
32
  # after_commit
32
- attrs[:after_commit] = gh_pl["after"]
33
+ attrs['after_commit'] = gh_pl["after"]
33
34
 
34
- attrs[:payload] = gh_pl
35
+ attrs['payload'] = gh_pl
35
36
 
36
37
  return attrs
37
38
  end
@@ -41,7 +42,7 @@ module Octopusci
41
42
  # this method returns nil. Otherwise, this project returns a hash of the
42
43
  # project info that it found in the config.
43
44
  def self.get_project_info(project_name, project_owner)
44
- Octopusci::CONFIG["projects"].each do |proj|
45
+ Octopusci::Config["projects"].each do |proj|
45
46
  if (proj['name'] == project_name) && (proj['owner'] == project_owner)
46
47
  return proj
47
48
  end
@@ -58,7 +59,7 @@ module Octopusci
58
59
  end
59
60
 
60
61
  def self.workspace_path(stage)
61
- return Octopusci::CONFIG['general']['workspace_base_path'] + "/#{stage}"
62
+ return Octopusci::Config['general']['workspace_base_path'] + "/#{stage}"
62
63
  end
63
64
  end
64
65
  end
@@ -0,0 +1,70 @@
1
+ module Octopusci
2
+ class IO
3
+ attr_accessor :job
4
+
5
+ def initialize(job)
6
+ @job = job
7
+ end
8
+
9
+ def read_all_out
10
+ if File.exists?(abs_output_file_path)
11
+ cont = ""
12
+ f = File.open(abs_output_file_path, 'r')
13
+ cont = f.read()
14
+ f.close
15
+ return cont
16
+ else
17
+ return ""
18
+ end
19
+ end
20
+
21
+ def read_all_log
22
+ if File.exists?(abs_log_file_path)
23
+ return File.open(abs_log_file_path, 'r').read()
24
+ else
25
+ return ""
26
+ end
27
+ end
28
+
29
+ def write_out(msg="", &block)
30
+ write_raw_output(false, msg, &block)
31
+ end
32
+
33
+ def write_log(msg="", &block)
34
+ write_raw_output(true, msg, &block)
35
+ end
36
+
37
+ def write_raw_output(silently=false, msg="")
38
+ # Make sure that the directory structure is in place for the job output.
39
+ if !File.directory?(abs_output_base_path)
40
+ FileUtils.mkdir_p(abs_output_base_path)
41
+ end
42
+
43
+ # Run the command and output the output to the job file
44
+ out_f = if silently
45
+ File.open(abs_log_file_path, 'a')
46
+ else
47
+ File.open(abs_output_file_path, 'a')
48
+ end
49
+
50
+ yield(out_f) if block_given?
51
+ out_f << msg unless msg.nil? || msg.empty?
52
+
53
+ out_f.close
54
+ end
55
+
56
+ private
57
+
58
+ def abs_output_file_path
59
+ return "#{abs_output_base_path}/output.txt"
60
+ end
61
+
62
+ def abs_log_file_path
63
+ return "#{abs_output_base_path}/silent_output.txt"
64
+ end
65
+
66
+ def abs_output_base_path
67
+ return "#{Octopusci::Config['general']['workspace_base_path']}/jobs/#{@job['id']}"
68
+ end
69
+ end
70
+ end