tinyci 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.yardopts +1 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +97 -0
- data/Guardfile +7 -0
- data/LICENSE +21 -0
- data/README.md +211 -0
- data/Rakefile +1 -0
- data/bin/tinyci +10 -0
- data/lib/pidfile.rb +150 -0
- data/lib/tinyci/builders/rkt_builder.rb +31 -0
- data/lib/tinyci/builders/script_builder.rb +18 -0
- data/lib/tinyci/builders/test_builder.rb +11 -0
- data/lib/tinyci/cli.rb +104 -0
- data/lib/tinyci/config.rb +55 -0
- data/lib/tinyci/executor.rb +21 -0
- data/lib/tinyci/git_utils.rb +68 -0
- data/lib/tinyci/installer.rb +55 -0
- data/lib/tinyci/logging.rb +16 -0
- data/lib/tinyci/logo.txt +6 -0
- data/lib/tinyci/multi_logger.rb +49 -0
- data/lib/tinyci/runner.rb +154 -0
- data/lib/tinyci/scheduler.rb +105 -0
- data/lib/tinyci/subprocesses.rb +111 -0
- data/lib/tinyci/symbolize.rb +22 -0
- data/lib/tinyci/testers/rkt_tester.rb +40 -0
- data/lib/tinyci/testers/script_tester.rb +18 -0
- data/lib/tinyci/testers/test_tester.rb +11 -0
- data/lib/tinyci/version.rb +3 -0
- data/tinyci.gemspec +44 -0
- metadata +179 -0
data/lib/tinyci/cli.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'tinyci/version'
|
2
|
+
require 'tinyci/scheduler'
|
3
|
+
require 'tinyci/installer'
|
4
|
+
require 'tinyci/git_utils'
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
module TinyCI
|
8
|
+
# Defines the CLI interface. Uses OptionParser.
|
9
|
+
class CLI
|
10
|
+
extend GitUtils
|
11
|
+
|
12
|
+
LOGO = File.read(File.expand_path('logo.txt', __dir__))
|
13
|
+
|
14
|
+
def self.parse!(argv = ARGV)
|
15
|
+
opts = {}
|
16
|
+
|
17
|
+
global = OptionParser.new do |o|
|
18
|
+
o.banner = ''
|
19
|
+
o.on("-q", "--[no-]quiet", "surpress output") {|q| opts[:quiet] = q}
|
20
|
+
o.on("-D <DIR>", "--dir <DIR>", "specify repository location") {|d| opts[:dir] = d}
|
21
|
+
end
|
22
|
+
|
23
|
+
subcommands = {
|
24
|
+
'run' => OptionParser.new do |o|
|
25
|
+
o.banner = "#{LOGO % TinyCI::VERSION}\nGlobal options:\n #{global.help.slice(3..-1)}\nrun options:"
|
26
|
+
o.on("-c <SHA>", "--commit <SHA>", "run against a specific commit") {|c| opts[:commit] = c}
|
27
|
+
o.on("-a", "--all", "run against all commits which have not been run against before") {|a| opts[:all] = a}
|
28
|
+
end,
|
29
|
+
'install' => OptionParser.new do |o|
|
30
|
+
o.banner = "Usage: install [options]"
|
31
|
+
o.on("-q", "--[no-]quiet", "quietly run") {|v| opts[:quiet] = v}
|
32
|
+
end
|
33
|
+
}
|
34
|
+
|
35
|
+
banner = <<TXT
|
36
|
+
#{LOGO % TinyCI::VERSION}
|
37
|
+
Global options:
|
38
|
+
#{global.help.strip}
|
39
|
+
|
40
|
+
Available commands:
|
41
|
+
run build and test the repo
|
42
|
+
install install the git hook into the current repository
|
43
|
+
version print the TinyCI version number
|
44
|
+
TXT
|
45
|
+
if argv[0] == '--help'
|
46
|
+
puts banner
|
47
|
+
return false
|
48
|
+
end
|
49
|
+
|
50
|
+
global.order!(argv)
|
51
|
+
command = argv.shift
|
52
|
+
|
53
|
+
if command.nil? || subcommands[command].nil?
|
54
|
+
puts banner
|
55
|
+
return false
|
56
|
+
end
|
57
|
+
|
58
|
+
subcommands[command].order!(argv)
|
59
|
+
|
60
|
+
opts[:dir] ||= begin
|
61
|
+
repo_root
|
62
|
+
rescue TinyCI::Subprocesses::SubprocessError => e
|
63
|
+
if e.message == '`git rev-parse --is-inside-git-dir` failed with code 32768'
|
64
|
+
exit 1
|
65
|
+
else
|
66
|
+
raise e
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
send "do_#{command}", opts
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.do_run(opts)
|
74
|
+
if PidFile.running?
|
75
|
+
puts 'TinyCI is already running!' unless opts[:quiet]
|
76
|
+
return false
|
77
|
+
end
|
78
|
+
|
79
|
+
opts.delete(:commit) if opts[:all]
|
80
|
+
|
81
|
+
if !opts[:commit] && !opts[:all]
|
82
|
+
puts "You must pass either --commit or --all, or try --help" unless opts[:quiet]
|
83
|
+
return false
|
84
|
+
end
|
85
|
+
|
86
|
+
logger = MultiLogger.new(quiet: opts[:quiet])
|
87
|
+
result = Scheduler.new(commit: opts[:commit], logger: logger, working_dir: opts[:dir]).run!
|
88
|
+
|
89
|
+
result
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.do_install(opts)
|
93
|
+
logger = MultiLogger.new(quiet: opts[:quiet])
|
94
|
+
|
95
|
+
TinyCI::Installer.new(logger: logger, working_dir: opts[:dir]).write!
|
96
|
+
end
|
97
|
+
|
98
|
+
def do_version(opts)
|
99
|
+
puts TinyCI::VERSION
|
100
|
+
|
101
|
+
true
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'tinyci/symbolize'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module TinyCI
|
5
|
+
# Represents the Configuration for a repo, parsed from the `.tinyci.yml` file in the repo root.
|
6
|
+
# Mainly a wrapper around a the hash object parsed from the yaml in the config file.
|
7
|
+
# The keys of the hash are recursively symbolized.
|
8
|
+
class Config
|
9
|
+
include Symbolize
|
10
|
+
|
11
|
+
# Constructor
|
12
|
+
#
|
13
|
+
# @param [String] working_dir The working directory in which to find the config file
|
14
|
+
# @param [String] config_path Override the path to the config file
|
15
|
+
# @param [String] config Override the config content
|
16
|
+
#
|
17
|
+
# @raise [ConfigMissingError] if the config file is not found
|
18
|
+
def initialize(working_dir: '.', config_path: nil, config: nil)
|
19
|
+
@working_dir = working_dir
|
20
|
+
@config_pathname = config_path
|
21
|
+
@config_content = config
|
22
|
+
|
23
|
+
raise ConfigMissingError, "config file #{config_pathname} not found" unless config_file_exists?
|
24
|
+
end
|
25
|
+
|
26
|
+
# Address into the config object
|
27
|
+
#
|
28
|
+
# @param [Symbol] key The key to address
|
29
|
+
def [](key)
|
30
|
+
config_content[key]
|
31
|
+
end
|
32
|
+
|
33
|
+
# Return the raw hash representation
|
34
|
+
def to_hash
|
35
|
+
config_content
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def config_file_exists?
|
41
|
+
File.exist? config_pathname
|
42
|
+
end
|
43
|
+
|
44
|
+
def config_pathname
|
45
|
+
@config_pathname || File.expand_path('.tinyci.yml', @working_dir)
|
46
|
+
end
|
47
|
+
|
48
|
+
def config_content
|
49
|
+
@config_content ||= symbolize(YAML.safe_load(File.read(config_pathname))).freeze
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Error raised when the config file cannot be found
|
54
|
+
class ConfigMissingError < StandardError; end
|
55
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'tinyci/subprocesses'
|
2
|
+
require 'tinyci/logging'
|
3
|
+
|
4
|
+
module TinyCI
|
5
|
+
# Parent class for Builder and Tester classes
|
6
|
+
#
|
7
|
+
# @abstract
|
8
|
+
class Executor
|
9
|
+
include Subprocesses
|
10
|
+
include Logging
|
11
|
+
|
12
|
+
# Returns a new instance of the executor.
|
13
|
+
#
|
14
|
+
# @param config [Hash] Configuration hash, typically taken from relevant key in the {Config} object.
|
15
|
+
# @param logger [Logger] Logger object
|
16
|
+
def initialize(config, logger: nil)
|
17
|
+
@config = config
|
18
|
+
@logger = logger
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'tinyci/subprocesses'
|
2
|
+
|
3
|
+
module TinyCI
|
4
|
+
|
5
|
+
# Methods for dealing with git repos.
|
6
|
+
module GitUtils
|
7
|
+
|
8
|
+
# Returns the absolute path to the root of the current git directory
|
9
|
+
#
|
10
|
+
# @return [String] the path
|
11
|
+
def repo_root
|
12
|
+
return git_directory_path if inside_bare_repo?
|
13
|
+
|
14
|
+
if inside_git_directory?
|
15
|
+
File.expand_path('..', git_directory_path)
|
16
|
+
elsif inside_work_tree?
|
17
|
+
execute(git_cmd('rev-parse', '--show-toplevel'))
|
18
|
+
else
|
19
|
+
raise 'not in git directory or work tree!?'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Are we currently under a git repo?
|
24
|
+
def inside_git_directory?
|
25
|
+
execute(git_cmd('rev-parse', '--is-inside-git-dir')) == 'true'
|
26
|
+
end
|
27
|
+
|
28
|
+
# Are we under a bare repo?
|
29
|
+
def inside_bare_repo?
|
30
|
+
execute(git_cmd('rev-parse', '--is-bare-repository')) == 'true'
|
31
|
+
end
|
32
|
+
|
33
|
+
# Are we currently under a git work tree?
|
34
|
+
def inside_work_tree?
|
35
|
+
execute(git_cmd('rev-parse', '--is-inside-work-tree')) == 'true'
|
36
|
+
end
|
37
|
+
|
38
|
+
# Are we currently under a repo in any sense?
|
39
|
+
def inside_repository?
|
40
|
+
execute(git_cmd('rev-parse', '--is-inside-work-tree', '--is-inside-git-dir')).split.any? {|s| s == 'true'}
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the absolute path to the .git directory
|
44
|
+
def git_directory_path
|
45
|
+
File.expand_path(execute(git_cmd('rev-parse', '--git-dir')), defined?(@working_dir) ? @working_dir : nil)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Execute a git command, passing the -C parameter if the current object has
|
49
|
+
# the working_directory instance var set
|
50
|
+
def git_cmd(*args)
|
51
|
+
cmd = ['git']
|
52
|
+
cmd += ['-C', @working_dir] if defined?(@working_dir) && !@working_dir.nil?
|
53
|
+
cmd += args
|
54
|
+
|
55
|
+
cmd
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def self.included(base)
|
61
|
+
base.include Subprocesses
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.extended(base)
|
65
|
+
base.extend Subprocesses
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'tinyci/git_utils'
|
3
|
+
|
4
|
+
module TinyCI
|
5
|
+
|
6
|
+
# Responsible for writing the git hook file
|
7
|
+
class Installer
|
8
|
+
include GitUtils
|
9
|
+
|
10
|
+
# Constructor
|
11
|
+
#
|
12
|
+
# @param [String] working_dir The directory from which to run. Does not have to be the root of the repo
|
13
|
+
# @param [Logger] logger Logger object
|
14
|
+
def initialize(working_dir: nil, logger: nil)
|
15
|
+
@logger = logger
|
16
|
+
@working_dir = working_dir || repo_root
|
17
|
+
end
|
18
|
+
|
19
|
+
# Write the hook to the relevant path and make it executable
|
20
|
+
def write!
|
21
|
+
unless inside_repository?
|
22
|
+
log_error "not currently in a git repository"
|
23
|
+
return false
|
24
|
+
end
|
25
|
+
|
26
|
+
if hook_exists?
|
27
|
+
log_error "post-update hook already exists in this repository"
|
28
|
+
return false
|
29
|
+
end
|
30
|
+
|
31
|
+
File.open(hook_path, 'a') {|f| f.write hook_content}
|
32
|
+
FileUtils.chmod('u+x', hook_path)
|
33
|
+
|
34
|
+
log_info 'tinyci post-update hook installed successfully'
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def hook_exists?
|
40
|
+
File.exist? hook_path
|
41
|
+
end
|
42
|
+
|
43
|
+
def hook_path
|
44
|
+
File.expand_path('hooks/post-update', git_directory_path)
|
45
|
+
end
|
46
|
+
|
47
|
+
def hook_content
|
48
|
+
<<-EOF
|
49
|
+
#!/bin/sh
|
50
|
+
|
51
|
+
tinyci run --all
|
52
|
+
EOF
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'tinyci/multi_logger'
|
2
|
+
|
3
|
+
module TinyCI
|
4
|
+
# Defines helper instance methods for logging to reduce code verbosity
|
5
|
+
module Logging
|
6
|
+
|
7
|
+
%w(log debug info warn error fatal unknown).each do |m|
|
8
|
+
define_method("log_#{m}") do |*args|
|
9
|
+
return false unless defined?(@logger) && @logger.is_a?(MultiLogger)
|
10
|
+
|
11
|
+
@logger.send(m, *args)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
data/lib/tinyci/logo.txt
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module TinyCI
|
4
|
+
# This class allows logging to both `STDOUT` and to a file with a single call.
|
5
|
+
# @attr [Boolean] quiet Disables logging to STDOUT
|
6
|
+
class MultiLogger
|
7
|
+
FORMAT = Proc.new do |severity, datetime, progname, msg|
|
8
|
+
"[#{datetime.strftime "%T"}] #{msg}\n"
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_accessor :quiet
|
12
|
+
|
13
|
+
# Constructor
|
14
|
+
#
|
15
|
+
# @param [Boolean] quiet Disables logging to STDOUT
|
16
|
+
# @param [String] path Location to write logfile to
|
17
|
+
def initialize(quiet: false, path: nil)
|
18
|
+
@file_logger = nil
|
19
|
+
self.output_path = path
|
20
|
+
@quiet = quiet
|
21
|
+
|
22
|
+
@stdout_logger = Logger.new($stdout)
|
23
|
+
@stdout_logger.formatter = FORMAT
|
24
|
+
@stdout_logger.level = Logger::INFO
|
25
|
+
end
|
26
|
+
|
27
|
+
def targets
|
28
|
+
logs = []
|
29
|
+
logs << @file_logger if @file_logger
|
30
|
+
logs << @stdout_logger unless @quiet
|
31
|
+
|
32
|
+
logs
|
33
|
+
end
|
34
|
+
|
35
|
+
def output_path=(path)
|
36
|
+
if path
|
37
|
+
@file_logger = Logger.new(path)
|
38
|
+
@file_logger.formatter = FORMAT
|
39
|
+
@file_logger.level = Logger::INFO
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
%w(log debug info warn error fatal unknown).each do |m|
|
44
|
+
define_method(m) do |*args|
|
45
|
+
targets.each { |t| t.send(m, *args) }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'tinyci/subprocesses'
|
2
|
+
require 'tinyci/git_utils'
|
3
|
+
require 'tinyci/logging'
|
4
|
+
require 'tinyci/config'
|
5
|
+
|
6
|
+
require 'tinyci/builders/test_builder'
|
7
|
+
require 'tinyci/testers/test_tester'
|
8
|
+
|
9
|
+
require 'tinyci/builders/rkt_builder'
|
10
|
+
require 'tinyci/testers/rkt_tester'
|
11
|
+
|
12
|
+
require 'tinyci/builders/script_builder'
|
13
|
+
require 'tinyci/testers/script_tester'
|
14
|
+
|
15
|
+
require 'fileutils'
|
16
|
+
|
17
|
+
module TinyCI
|
18
|
+
# Responsible for managing the running of TinyCI against a single git object.
|
19
|
+
#
|
20
|
+
# @attr builder [TinyCI::Executor] Returns the Builder object. Used solely for testing at this time.
|
21
|
+
# @attr tester [TinyCI::Executor] Returns the Tester object. Used solely for testing at this time.
|
22
|
+
class Runner
|
23
|
+
include Subprocesses
|
24
|
+
include GitUtils
|
25
|
+
include Logging
|
26
|
+
|
27
|
+
attr_accessor :builder, :tester
|
28
|
+
|
29
|
+
# Constructor, allows injection of generic configuration params.
|
30
|
+
#
|
31
|
+
# @param working_dir [String] The working directory to execute against.
|
32
|
+
# @param commit [String] SHA1 of git object to run against
|
33
|
+
# @param logger [Logger] Logger object
|
34
|
+
# @param time [Time] Override time of object creation. Used solely for testing at this time.
|
35
|
+
# @param config [Hash] Override TinyCI config object, normally loaded from `.tinyci` file. Used solely for testing at this time.
|
36
|
+
def initialize(working_dir: '.', commit:, time: nil, logger: nil, config: nil)
|
37
|
+
@working_dir = working_dir
|
38
|
+
@logger = logger
|
39
|
+
@config = config
|
40
|
+
@commit = commit
|
41
|
+
@time = time || commit_time
|
42
|
+
end
|
43
|
+
|
44
|
+
# Runs the TinyCI system against the single git object referenced in `@commit`.
|
45
|
+
#
|
46
|
+
# @return [Boolean] `true` if the commit was built and tested successfully, `false` otherwise
|
47
|
+
def run!
|
48
|
+
begin
|
49
|
+
ensure_path target_path
|
50
|
+
setup_log
|
51
|
+
|
52
|
+
log_info "Commit: #{@commit}"
|
53
|
+
|
54
|
+
log_info "Cleaning..."
|
55
|
+
clean
|
56
|
+
|
57
|
+
log_info "Exporting..."
|
58
|
+
ensure_path export_path
|
59
|
+
export
|
60
|
+
|
61
|
+
begin
|
62
|
+
load_config
|
63
|
+
rescue ConfigMissingError => e
|
64
|
+
log_error e.message
|
65
|
+
log_error 'Removing export...'
|
66
|
+
clean
|
67
|
+
|
68
|
+
return false
|
69
|
+
end
|
70
|
+
@builder ||= instantiate_builder
|
71
|
+
@tester ||= instantiate_tester
|
72
|
+
|
73
|
+
log_info "Building..."
|
74
|
+
@builder.build
|
75
|
+
|
76
|
+
log_info "Testing..."
|
77
|
+
@tester.test
|
78
|
+
|
79
|
+
log_info "Finished #{@commit}"
|
80
|
+
rescue => e
|
81
|
+
raise e if ENV['TINYCI_ENV'] == 'test'
|
82
|
+
|
83
|
+
log_error e
|
84
|
+
log_error e.backtrace
|
85
|
+
return false
|
86
|
+
ensure
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
true
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
# Creates log file if it doesnt exist
|
96
|
+
def setup_log
|
97
|
+
return unless @logger.is_a? MultiLogger
|
98
|
+
FileUtils.touch logfile_path
|
99
|
+
@logger.output_path = logfile_path
|
100
|
+
end
|
101
|
+
|
102
|
+
def logfile_path
|
103
|
+
File.join(target_path, 'tinyci.log')
|
104
|
+
end
|
105
|
+
|
106
|
+
# Instantiate the Builder object according to the class named in the config
|
107
|
+
def instantiate_builder
|
108
|
+
klass = TinyCI::Builders.const_get(@config[:builder][:class])
|
109
|
+
klass.new(@config[:builder][:config].merge(target: export_path), logger: @logger)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Instantiate the Tester object according to the class named in the config
|
113
|
+
def instantiate_tester
|
114
|
+
klass = TinyCI::Testers.const_get(@config[:tester][:class])
|
115
|
+
klass.new(@config[:tester][:config].merge(target: export_path), logger: @logger)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Instantiate the {Config} object from the `.tinyci.yml` file in the exported directory
|
119
|
+
def load_config
|
120
|
+
@config ||= Config.new(working_dir: export_path)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Parse the commit time from git
|
124
|
+
def commit_time
|
125
|
+
Time.at execute(git_cmd('show', '-s', '--format=%ct', @commit)).to_i
|
126
|
+
end
|
127
|
+
|
128
|
+
# Build the absolute target path
|
129
|
+
def target_path
|
130
|
+
File.absolute_path("#{@working_dir}/builds/#{@time.to_i}_#{@commit}/")
|
131
|
+
end
|
132
|
+
|
133
|
+
# Build the export path
|
134
|
+
def export_path
|
135
|
+
File.join(target_path, 'export')
|
136
|
+
end
|
137
|
+
|
138
|
+
def ensure_path(path)
|
139
|
+
execute 'mkdir', '-p', path
|
140
|
+
end
|
141
|
+
|
142
|
+
def clean
|
143
|
+
FileUtils.rm_rf export_path
|
144
|
+
end
|
145
|
+
|
146
|
+
# Export a clean copy of the repo at the given commit, without a .git directory etc.
|
147
|
+
# This implementation is slightly hacky but its the cleanest way to do it in the absence of
|
148
|
+
# a `git export` subcommand.
|
149
|
+
# see https://stackoverflow.com/a/163769
|
150
|
+
def export
|
151
|
+
execute_pipe git_cmd('archive', '--format=tar', @commit), ['tar', '-C', export_path, '-xf', '-']
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'tinyci/runner'
|
2
|
+
require 'tinyci/subprocesses'
|
3
|
+
require 'tinyci/git_utils'
|
4
|
+
require 'pidfile'
|
5
|
+
|
6
|
+
module TinyCI
|
7
|
+
# Manages the execution of test jobs. Responsible for deciding which
|
8
|
+
# commits need to be built and tested. Also manages the pidfile. This
|
9
|
+
# is the main entrypoint for TinyCI.
|
10
|
+
#
|
11
|
+
# @attr_reader [String] working_dir The working directory to execute against
|
12
|
+
class Scheduler
|
13
|
+
include Subprocesses
|
14
|
+
include Logging
|
15
|
+
include GitUtils
|
16
|
+
|
17
|
+
attr_reader :working_dir
|
18
|
+
|
19
|
+
# Constructor, allows injection of configuration and custom {Runner} class.
|
20
|
+
# Config params are passed to {Runner} instances.
|
21
|
+
#
|
22
|
+
# @param working_dir [String] The working directory to execute against
|
23
|
+
# @param logger [Logger] Logger object
|
24
|
+
# @param commit [String] specific git object to run against
|
25
|
+
# @param runner_class [TinyCI::Runner] Injection of {Runner} dependency
|
26
|
+
def initialize(
|
27
|
+
working_dir: nil,
|
28
|
+
logger: nil,
|
29
|
+
commit: nil,
|
30
|
+
runner_class: Runner
|
31
|
+
)
|
32
|
+
|
33
|
+
@working_dir = working_dir || repo_root
|
34
|
+
@logger = logger
|
35
|
+
@runner_class = runner_class
|
36
|
+
@commit = commit
|
37
|
+
end
|
38
|
+
|
39
|
+
# Runs the TinyCI system against the relevant commits. Also sets up the pidfile.
|
40
|
+
#
|
41
|
+
# @return [Boolean] `true` if all commits built and tested successfully, `false` otherwise
|
42
|
+
def run!
|
43
|
+
pid = PidFile.new(pidfile: 'tinyci.pid', piddir: @working_dir)
|
44
|
+
|
45
|
+
result = if @commit
|
46
|
+
run_commit @commit
|
47
|
+
else
|
48
|
+
run_all_commits
|
49
|
+
end
|
50
|
+
|
51
|
+
pid.release
|
52
|
+
|
53
|
+
result
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# Git objects to be executed against, all those without a tinyci tag
|
59
|
+
#
|
60
|
+
# @return [Array<String>] the sha1 hashes in reverse order of creation time
|
61
|
+
def get_commits
|
62
|
+
log = execute(git_cmd('log', '--notes=tinyci*', '--format=%H %ct %N§§§', '--reverse'))
|
63
|
+
|
64
|
+
lines = log.split("§§§")
|
65
|
+
lines.map do |line|
|
66
|
+
parts = line.split(' ')
|
67
|
+
{
|
68
|
+
sha: parts[0],
|
69
|
+
time: parts[1],
|
70
|
+
result: parts[2]
|
71
|
+
}
|
72
|
+
end.select {|c| c[:result].nil?}
|
73
|
+
end
|
74
|
+
|
75
|
+
# Instantiates {Runner} for a given git object, runs it, and stores the result
|
76
|
+
def run_commit(commit)
|
77
|
+
result = @runner_class.new(
|
78
|
+
working_dir: @working_dir,
|
79
|
+
commit: commit[:sha],
|
80
|
+
time: commit[:time],
|
81
|
+
logger: @logger
|
82
|
+
).run!
|
83
|
+
|
84
|
+
set_result(commit, result)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Repeatedly gets the list of eligable commits and runs TinyCI against them until there are no more remaining
|
88
|
+
def run_all_commits
|
89
|
+
commits = get_commits
|
90
|
+
|
91
|
+
until commits.empty? do
|
92
|
+
commits.each {|c| run_commit(c)}
|
93
|
+
|
94
|
+
commits = get_commits
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Stores the result in a git note
|
99
|
+
def set_result(commit, result)
|
100
|
+
result_message = result ? 'success' : 'failure'
|
101
|
+
|
102
|
+
execute git_cmd('notes', '--ref', 'tinyci-result', 'add', '-m', result_message, commit[:sha])
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|