octopusci 0.2.3 → 0.3.0

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 (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