citrus-core 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.citrus/config.rb +3 -0
  3. data/.gitignore +21 -0
  4. data/.rspec +2 -0
  5. data/.travis.yml +9 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +10 -0
  9. data/Rakefile +9 -0
  10. data/citrus-core.gemspec +25 -0
  11. data/examples/bootstrap.rb +40 -0
  12. data/examples/payload.json +1 -0
  13. data/examples/web.rb +53 -0
  14. data/lib/citrus/core.rb +37 -0
  15. data/lib/citrus/core/build.rb +16 -0
  16. data/lib/citrus/core/cached_code_fetcher.rb +34 -0
  17. data/lib/citrus/core/changeset.rb +25 -0
  18. data/lib/citrus/core/commit.rb +20 -0
  19. data/lib/citrus/core/commit_changes.rb +15 -0
  20. data/lib/citrus/core/configuration.rb +17 -0
  21. data/lib/citrus/core/configuration_loader.rb +26 -0
  22. data/lib/citrus/core/configuration_validator.rb +13 -0
  23. data/lib/citrus/core/execute_build_service.rb +30 -0
  24. data/lib/citrus/core/git_adapter.rb +43 -0
  25. data/lib/citrus/core/github_adapter.rb +21 -0
  26. data/lib/citrus/core/publisher.rb +23 -0
  27. data/lib/citrus/core/repository.rb +13 -0
  28. data/lib/citrus/core/test_result.rb +24 -0
  29. data/lib/citrus/core/test_runner.rb +32 -0
  30. data/lib/citrus/core/version.rb +5 -0
  31. data/lib/citrus/core/workspace_builder.rb +27 -0
  32. data/spec/build_spec.rb +13 -0
  33. data/spec/cached_code_fetcher_spec.rb +69 -0
  34. data/spec/changeset_spec.rb +15 -0
  35. data/spec/citrus_spec.rb +18 -0
  36. data/spec/cofiguration_loader_spec.rb +27 -0
  37. data/spec/cofiguration_spec.rb +11 -0
  38. data/spec/cofiguration_validator_spec.rb +28 -0
  39. data/spec/commit_changes_spec.rb +15 -0
  40. data/spec/commit_spec.rb +21 -0
  41. data/spec/execute_build_service_spec.rb +76 -0
  42. data/spec/fixtures/github_push_data.json +143 -0
  43. data/spec/fixtures/repo/.citrus/config.rb +3 -0
  44. data/spec/github_adapter_spec.rb +16 -0
  45. data/spec/publisher_spec.rb +26 -0
  46. data/spec/repository_spec.rb +11 -0
  47. data/spec/spec_helper.rb +11 -0
  48. data/spec/test_result_spec.rb +20 -0
  49. data/spec/test_runner_spec.rb +44 -0
  50. data/spec/workspace_builder_spec.rb +55 -0
  51. metadata +167 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0bb11322dec0a84ab1f72b581194f927e7514e01
4
+ data.tar.gz: 25db9393f02e6b1451706edd8b27460a44634f92
5
+ SHA512:
6
+ metadata.gz: 1b32d90ae4f2d5f830f57c24d18880050b5fd652224c3a37372f9b99d418ace48318fcc37f1e0cab972685600d7e9ceeed9e99edbd31d5637566da10a0be2109
7
+ data.tar.gz: ee4ce1f0ca35d4b40dd979123866ab6e20bfb82f3a8dff1b5a9073a4ea01afab5ac4076d7c96e9d2f6329b50abd15be4efdc6972ced75442f43979e438871611
@@ -0,0 +1,3 @@
1
+ Citrus::Configuration.describe do |c|
2
+ c.build_script = "rspec"
3
+ end
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ bin
19
+ .ruby-version
20
+ cache
21
+ builds
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ script: bundle exec rspec
3
+ rvm:
4
+ - 1.9.2
5
+ - 1.9.3
6
+ - 2.0.0
7
+ - rbx-19mode
8
+ - jruby-19mode
9
+
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Paweł Pacana
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.
@@ -0,0 +1,10 @@
1
+ ## Citrus Core
2
+
3
+ [![Build Status](https://secure.travis-ci.org/pawelpacana/citrus-core.png)](http://travis-ci.org/pawelpacana/citrus-core) [![Dependency Status](https://gemnasium.com/pawelpacana/citrus-core.png)](https://gemnasium.com/pawelpacana/citrus-core) [![Code Climate](https://codeclimate.com/github/pawelpacana/citrus-core.png)](https://codeclimate.com/github/drugpl/bbq) [![Gem Version](https://badge.fury.io/rb/citrus-core.png)](http://badge.fury.io/rb/citrus-core)
4
+
5
+ Spartan CI environment. Watch it build itself:
6
+
7
+ bundle install
8
+ bundle exec ruby -Ilib examples/bootstrap.rb
9
+
10
+
@@ -0,0 +1,9 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs = %w(lib test)
6
+ t.pattern = 'test/*_test.rb'
7
+ end
8
+
9
+ task :default => :test
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'citrus/core/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "citrus-core"
8
+ gem.version = Citrus::Core::VERSION
9
+ gem.authors = ["Paweł Pacana"]
10
+ gem.email = ["pawel.pacana@syswise.eu"]
11
+ gem.description = "Citrus continous integration core components."
12
+ gem.summary = "Citrus continous integration core components."
13
+ gem.homepage = "http://citrus-ci.org"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency 'childprocess', '~> 0.3.9'
21
+
22
+ gem.add_development_dependency 'fakefs', '~> 0.4.2'
23
+ gem.add_development_dependency 'rspec', '~> 2.13'
24
+ gem.add_development_dependency 'bogus', '~> 0.0.4'
25
+ end
@@ -0,0 +1,40 @@
1
+ require 'citrus/core'
2
+ require 'pathname'
3
+
4
+ class Notifier
5
+ def build_succeeded(build, output)
6
+ puts "[#{build.uuid}] Build has succeeded."
7
+ end
8
+
9
+ def build_failed(build, output)
10
+ puts "[#{build.uuid}] Build has failed."
11
+ end
12
+
13
+ def build_aborted(build, error)
14
+ puts "[#{build.uuid}] Build has been aborted."
15
+ end
16
+
17
+ def build_started(build)
18
+ puts "[#{build.uuid}] Build has started."
19
+ end
20
+
21
+ def output_received(data)
22
+ print data
23
+ end
24
+ end
25
+
26
+ include Citrus::Core
27
+
28
+ payload = Pathname.new(File.dirname(__FILE__)).join('payload.json').read
29
+ changeset = GithubAdapter.new.create_changeset_from_push_data(payload)
30
+ build = Build.new(changeset)
31
+ workspace_builder = WorkspaceBuilder.new
32
+ configuration_loader = ConfigurationLoader.new
33
+ test_runner = TestRunner.new
34
+ subscriber = Notifier.new
35
+ test_runner.add_subscriber(subscriber)
36
+ build_service = ExecuteBuildService.new(workspace_builder, configuration_loader, test_runner)
37
+ build_service.add_subscriber(subscriber)
38
+ build_service.start(build)
39
+
40
+
@@ -0,0 +1 @@
1
+ { "after": "1b234fec1196170fa01c7b7d370a8fec7e2e3e61", "before": "646f122aa8fa537f9d05bbcc58712f782ee9d43f", "commits": [ { "added": [], "author": { "email": "pawel.pacana@gmail.com", "name": "Pawe\u0142 Pacana", "username": "pawelpacana" }, "committer": { "email": "pawel.pacana@gmail.com", "name": "Pawe\u0142 Pacana", "username": "pawelpacana" }, "distinct": true, "id": "0332491c0d3b617539c29fa66b0e6316e23b25a2", "message": "Add cache_root global.", "modified": [ ".gitignore", "lib/citrus/core.rb" ], "removed": [], "timestamp": "2013-05-29T15:30:43-07:00", "url": "https://github.com/pawelpacana/citrus-core/commit/0332491c0d3b617539c29fa66b0e6316e23b25a2" }, { "added": [], "author": { "email": "pawel.pacana@gmail.com", "name": "Pawe\u0142 Pacana", "username": "pawelpacana" }, "committer": { "email": "pawel.pacana@gmail.com", "name": "Pawe\u0142 Pacana", "username": "pawelpacana" }, "distinct": true, "id": "cb1eb5365799df518cfcfa4e422aafe06e4b64d5", "message": "Bring git adapter to reality since it did not have spec.", "modified": [ "lib/citrus/core/git_adapter.rb" ], "removed": [], "timestamp": "2013-05-29T15:32:07-07:00", "url": "https://github.com/pawelpacana/citrus-core/commit/cb1eb5365799df518cfcfa4e422aafe06e4b64d5" }, { "added": [ ".citrus/config.rb" ], "author": { "email": "pawel.pacana@gmail.com", "name": "Pawe\u0142 Pacana", "username": "pawelpacana" }, "committer": { "email": "pawel.pacana@gmail.com", "name": "Pawe\u0142 Pacana", "username": "pawelpacana" }, "distinct": true, "id": "1b234fec1196170fa01c7b7d370a8fec7e2e3e61", "message": "Add simplest config to run build from this repository.", "modified": [], "removed": [], "timestamp": "2013-05-29T15:33:17-07:00", "url": "https://github.com/pawelpacana/citrus-core/commit/1b234fec1196170fa01c7b7d370a8fec7e2e3e61" } ], "compare": "https://github.com/pawelpacana/citrus-core/compare/646f122aa8fa...1b234fec1196", "created": false, "deleted": false, "forced": false, "head_commit": { "added": [ ".citrus/config.rb" ], "author": { "email": "pawel.pacana@gmail.com", "name": "Pawe\u0142 Pacana", "username": "pawelpacana" }, "committer": { "email": "pawel.pacana@gmail.com", "name": "Pawe\u0142 Pacana", "username": "pawelpacana" }, "distinct": true, "id": "1b234fec1196170fa01c7b7d370a8fec7e2e3e61", "message": "Add simplest config to run build from this repository.", "modified": [], "removed": [], "timestamp": "2013-05-29T15:33:17-07:00", "url": "https://github.com/pawelpacana/citrus-core/commit/1b234fec1196170fa01c7b7d370a8fec7e2e3e61" }, "pusher": { "email": "pawel.pacana@gmail.com", "name": "pawelpacana" }, "ref": "refs/heads/master", "repository": { "created_at": 1360404806, "fork": false, "forks": 0, "has_downloads": true, "has_issues": true, "has_wiki": true, "id": 8108400, "language": "Ruby", "master_branch": "master", "name": "citrus-core", "open_issues": 0, "owner": { "email": "pawel.pacana@gmail.com", "name": "pawelpacana" }, "private": false, "pushed_at": 1369866841, "size": 220, "stargazers": 1, "url": "https://github.com/pawelpacana/citrus-core", "watchers": 1 } }
@@ -0,0 +1,53 @@
1
+ require 'sinatra'
2
+ require 'citrus/core'
3
+
4
+ class ConsoleNotifier
5
+ attr_reader :io
6
+
7
+ def initialize(io = STDOUT)
8
+ @io = io
9
+ end
10
+
11
+ def build_succeeded(build, output); io.puts "[#{build.uuid}] Build has succeeded."; end
12
+ def build_failed(build, output); io.puts "[#{build.uuid}] Build has failed."; end
13
+ def build_aborted(build, error) ; io.puts "[#{build.uuid}] Build has been aborted."; end
14
+ def build_started(build); io.puts "[#{build.uuid}] Build has started."; end
15
+ def output_received(data); io.print data; end
16
+ end
17
+
18
+ class QueuedBuilder
19
+ include Citrus::Core
20
+
21
+ attr_reader :queue, :service
22
+
23
+ def initialize(queue, subscriber)
24
+ workspace_builder = WorkspaceBuilder.new
25
+ configuration_loader = ConfigurationLoader.new
26
+ test_runner = TestRunner.new
27
+ test_runner.add_subscriber(subscriber)
28
+ @queue = queue
29
+ @service = ExecuteBuildService.new(workspace_builder, configuration_loader, test_runner)
30
+ @service.add_subscriber(subscriber)
31
+ end
32
+
33
+ def run
34
+ Thread.new do
35
+ loop do
36
+ build = queue.pop
37
+ service.start(build)
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ queue = Queue.new
44
+ builder = QueuedBuilder.new(queue, ConsoleNotifier.new)
45
+ builder.run
46
+
47
+ post '/github_push' do
48
+ adapter = Citrus::Core::GithubAdapter.new
49
+ changeset = adapter.create_changeset_from_push_data(params[:payload])
50
+ build = Citrus::Core::Build.new(changeset)
51
+ queue << build
52
+ status 200
53
+ end
@@ -0,0 +1,37 @@
1
+ require 'pathname'
2
+
3
+ module Citrus
4
+ module Core
5
+ class << self
6
+
7
+ def build_root
8
+ @build_root || root.join('builds')
9
+ end
10
+
11
+ def cache_root
12
+ @cache_root || root.join('cache')
13
+ end
14
+
15
+ def root
16
+ Pathname.new(File.expand_path('../../', File.dirname(__FILE__)))
17
+ end
18
+
19
+ end
20
+ end
21
+ end
22
+
23
+ require 'citrus/core/publisher'
24
+ require 'citrus/core/build'
25
+ require 'citrus/core/test_result'
26
+ require 'citrus/core/workspace_builder'
27
+ require 'citrus/core/cached_code_fetcher'
28
+ require 'citrus/core/changeset'
29
+ require 'citrus/core/test_runner'
30
+ require 'citrus/core/github_adapter'
31
+ require 'citrus/core/commit'
32
+ require 'citrus/core/commit_changes'
33
+ require 'citrus/core/configuration'
34
+ require 'citrus/core/configuration_loader'
35
+ require 'citrus/core/configuration_validator'
36
+ require 'citrus/core/git_adapter'
37
+ require 'citrus/core/execute_build_service'
@@ -0,0 +1,16 @@
1
+ require 'securerandom'
2
+
3
+ module Citrus
4
+ module Core
5
+ class Build
6
+
7
+ attr_reader :changeset, :uuid
8
+
9
+ def initialize(changeset, uuid = SecureRandom.uuid)
10
+ @changeset = changeset
11
+ @uuid = uuid
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,34 @@
1
+ require 'digest/sha1'
2
+
3
+ module Citrus
4
+ module Core
5
+ class CachedCodeFetcher
6
+
7
+ attr_reader :cache_root, :vcs_adapter
8
+
9
+ def initialize(cache_root = Citrus::Core.cache_root, vcs_adapter = GitAdapter.new)
10
+ @cache_root = cache_root
11
+ @vcs_adapter = vcs_adapter
12
+ end
13
+
14
+ def fetch(changeset, destination)
15
+ url = changeset.repository_url
16
+ head = changeset.head
17
+ cache_dir = cache_root.join(Digest::SHA1.hexdigest(url))
18
+ cache_dir.mkpath
19
+ update_cache(url, cache_dir)
20
+ vcs_adapter.clone_repository(cache_dir, destination)
21
+ vcs_adapter.checkout(destination, head)
22
+ end
23
+
24
+ protected
25
+
26
+ def update_cache(url, cache_dir)
27
+ return vcs_adapter.clone_repository(url, cache_dir) unless cache_dir.join('.git').exist?
28
+ vcs_adapter.fetch_remote(cache_dir)
29
+ vcs_adapter.reset(cache_dir)
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,25 @@
1
+ require 'citrus/core/commit'
2
+ require 'citrus/core/repository'
3
+
4
+ module Citrus
5
+ module Core
6
+ class Changeset
7
+
8
+ attr_reader :commits, :repository
9
+
10
+ def initialize(repository, commits)
11
+ @repository = repository
12
+ @commits = commits
13
+ end
14
+
15
+ def repository_url
16
+ repository.url
17
+ end
18
+
19
+ def head
20
+ commits.last.sha
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ require 'date'
2
+
3
+ module Citrus
4
+ module Core
5
+ class Commit
6
+
7
+ attr_reader :sha, :author, :message, :timestamp, :changes, :url
8
+
9
+ def initialize(sha, author, message, timestamp, changes, url = nil)
10
+ @sha = sha
11
+ @url = url
12
+ @author = author
13
+ @message = message
14
+ @changes = changes
15
+ @timestamp = timestamp
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ module Citrus
2
+ module Core
3
+ class CommitChanges
4
+
5
+ attr_reader :added, :removed, :modified
6
+
7
+ def initialize(added, removed, modified)
8
+ @added = added
9
+ @removed = removed
10
+ @modified = modified
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module Citrus
2
+ module Core
3
+ class Configuration
4
+
5
+ attr_accessor :build_script
6
+
7
+ def self.describe
8
+ config = self.new
9
+ yield config if block_given?
10
+ config
11
+ end
12
+
13
+ end
14
+ end
15
+
16
+ Configuration = Core::Configuration
17
+ end
@@ -0,0 +1,26 @@
1
+ module Citrus
2
+ module Core
3
+ class ConfigurationError < StandardError; end
4
+ class ConfigurationFileInvalidError < ConfigurationError; end
5
+ class ConfigurationFileNotFoundError < ConfigurationError; end
6
+
7
+ class ConfigurationLoader
8
+
9
+ attr_reader :validator
10
+
11
+ def initialize(validator = ConfigurationValidator.new)
12
+ @validator = validator
13
+ end
14
+
15
+ def load_from_path(pathname)
16
+ data = pathname.join('.citrus/config.rb').read
17
+ config = eval(data)
18
+ raise ConfigurationFileInvalidError unless validator.validate(config)
19
+ config
20
+ rescue Errno::ENOENT
21
+ raise ConfigurationFileNotFoundError
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,13 @@
1
+ module Citrus
2
+ module Core
3
+ class ConfigurationValidator
4
+
5
+ def validate(configuration)
6
+ Configuration === configuration &&
7
+ configuration.build_script &&
8
+ !configuration.build_script.empty?
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ require 'citrus/core'
2
+
3
+ module Citrus
4
+ module Core
5
+ class ExecuteBuildService
6
+ include Publisher
7
+
8
+ attr_reader :workspace_builder, :configuration_loader, :test_runner
9
+
10
+ def initialize(workspace_builder = WorkspaceBuilder.new, configuration_loader = ConfigurationLoader.new, test_runner = TestRunner.new)
11
+ @workspace_builder = workspace_builder
12
+ @configuration_loader = configuration_loader
13
+ @test_runner = test_runner
14
+ end
15
+
16
+ def start(build)
17
+ path = workspace_builder.create_workspace(build)
18
+ configuration = configuration_loader.load_from_path(path)
19
+ publish(:build_started, build)
20
+ result = test_runner.start(configuration, path)
21
+ publish(:build_succeeded, build, result.output) if result.success?
22
+ publish(:build_failed, build, result.output) if result.failure?
23
+ rescue ConfigurationError => error
24
+ publish(:build_aborted, build, error)
25
+ raise error
26
+ end
27
+
28
+ end
29
+ end
30
+ end