citrus-core 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.
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