citrus-core 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.citrus/config.rb +3 -0
- data/.gitignore +21 -0
- data/.rspec +2 -0
- data/.travis.yml +9 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +10 -0
- data/Rakefile +9 -0
- data/citrus-core.gemspec +25 -0
- data/examples/bootstrap.rb +40 -0
- data/examples/payload.json +1 -0
- data/examples/web.rb +53 -0
- data/lib/citrus/core.rb +37 -0
- data/lib/citrus/core/build.rb +16 -0
- data/lib/citrus/core/cached_code_fetcher.rb +34 -0
- data/lib/citrus/core/changeset.rb +25 -0
- data/lib/citrus/core/commit.rb +20 -0
- data/lib/citrus/core/commit_changes.rb +15 -0
- data/lib/citrus/core/configuration.rb +17 -0
- data/lib/citrus/core/configuration_loader.rb +26 -0
- data/lib/citrus/core/configuration_validator.rb +13 -0
- data/lib/citrus/core/execute_build_service.rb +30 -0
- data/lib/citrus/core/git_adapter.rb +43 -0
- data/lib/citrus/core/github_adapter.rb +21 -0
- data/lib/citrus/core/publisher.rb +23 -0
- data/lib/citrus/core/repository.rb +13 -0
- data/lib/citrus/core/test_result.rb +24 -0
- data/lib/citrus/core/test_runner.rb +32 -0
- data/lib/citrus/core/version.rb +5 -0
- data/lib/citrus/core/workspace_builder.rb +27 -0
- data/spec/build_spec.rb +13 -0
- data/spec/cached_code_fetcher_spec.rb +69 -0
- data/spec/changeset_spec.rb +15 -0
- data/spec/citrus_spec.rb +18 -0
- data/spec/cofiguration_loader_spec.rb +27 -0
- data/spec/cofiguration_spec.rb +11 -0
- data/spec/cofiguration_validator_spec.rb +28 -0
- data/spec/commit_changes_spec.rb +15 -0
- data/spec/commit_spec.rb +21 -0
- data/spec/execute_build_service_spec.rb +76 -0
- data/spec/fixtures/github_push_data.json +143 -0
- data/spec/fixtures/repo/.citrus/config.rb +3 -0
- data/spec/github_adapter_spec.rb +16 -0
- data/spec/publisher_spec.rb +26 -0
- data/spec/repository_spec.rb +11 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/test_result_spec.rb +20 -0
- data/spec/test_runner_spec.rb +44 -0
- data/spec/workspace_builder_spec.rb +55 -0
- metadata +167 -0
checksums.yaml
ADDED
@@ -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
|
data/.citrus/config.rb
ADDED
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
+
|
data/Rakefile
ADDED
data/citrus-core.gemspec
ADDED
@@ -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 } }
|
data/examples/web.rb
ADDED
@@ -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
|
data/lib/citrus/core.rb
ADDED
@@ -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,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,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,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
|