tinyci 0.4 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -2
- data/.rspec +2 -0
- data/.rubocop.yml +17 -2
- data/.ruby-version +1 -1
- data/.tinyci.yml +3 -1
- data/.yardopts +1 -0
- data/CHANGELOG.md +5 -0
- data/Dockerfile +3 -2
- data/Gemfile +2 -1
- data/Gemfile.lock +67 -39
- data/Guardfile +5 -3
- data/Rakefile +3 -1
- data/bin/tinyci +3 -2
- data/lib/pidfile.rb +28 -39
- data/lib/tinyci/builders/script_builder.rb +2 -0
- data/lib/tinyci/builders/test_builder.rb +3 -1
- data/lib/tinyci/cli.rb +159 -78
- data/lib/tinyci/cli_ssh_delegator.rb +111 -0
- data/lib/tinyci/compactor.rb +26 -26
- data/lib/tinyci/config.rb +22 -23
- data/lib/tinyci/config_transformer.rb +14 -16
- data/lib/tinyci/executor.rb +14 -12
- data/lib/tinyci/git_utils.rb +87 -18
- data/lib/tinyci/hookers/script_hooker.rb +26 -27
- data/lib/tinyci/installer.rb +28 -21
- data/lib/tinyci/log_viewer.rb +87 -0
- data/lib/tinyci/logging.rb +5 -5
- data/lib/tinyci/multi_logger.rb +30 -21
- data/lib/tinyci/path_utils.rb +44 -0
- data/lib/tinyci/runner.rb +95 -78
- data/lib/tinyci/scheduler.rb +44 -42
- data/lib/tinyci/subprocesses.rb +41 -32
- data/lib/tinyci/symbolize.rb +3 -1
- data/lib/tinyci/testers/script_tester.rb +2 -0
- data/lib/tinyci/testers/test_tester.rb +2 -0
- data/lib/tinyci/version.rb +3 -1
- data/lib/yard_plugin.rb +9 -1
- data/tinyci.gemspec +30 -22
- metadata +106 -19
data/lib/tinyci/cli.rb
CHANGED
@@ -1,118 +1,199 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'tinyci/version'
|
2
4
|
require 'tinyci/scheduler'
|
3
5
|
require 'tinyci/installer'
|
4
6
|
require 'tinyci/compactor'
|
7
|
+
require 'tinyci/log_viewer'
|
5
8
|
require 'tinyci/git_utils'
|
9
|
+
require 'tinyci/cli_ssh_delegator'
|
6
10
|
require 'optparse'
|
7
11
|
require 'pidfile'
|
8
12
|
|
9
13
|
module TinyCI
|
10
14
|
# Defines the CLI interface. Uses OptionParser.
|
11
15
|
class CLI
|
12
|
-
|
13
|
-
|
14
|
-
LOGO = File.read(File.expand_path('logo.txt', __dir__))
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
include GitUtils
|
17
|
+
|
18
|
+
LOGO = File.read(File.expand_path('logo.txt', __dir__)).freeze
|
19
|
+
HIDDEN_OPTIONS = %w[
|
20
|
+
running-remotely
|
21
|
+
].freeze
|
22
|
+
|
23
|
+
def initialize(argv = ARGV)
|
24
|
+
@argv = argv
|
25
|
+
@opts = {}
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse!
|
29
|
+
if @argv[0] == '--help'
|
30
|
+
puts banner
|
31
|
+
return false
|
32
|
+
end
|
33
|
+
|
34
|
+
unless subcommand
|
35
|
+
puts banner
|
36
|
+
return false
|
37
|
+
end
|
38
|
+
|
39
|
+
global_parser.order!(global_args)
|
40
|
+
subcommand_parsers[subcommand].order!(subcommand_args)
|
41
|
+
|
42
|
+
@opts[:working_dir] ||= working_dir
|
43
|
+
|
44
|
+
if @opts[:remote]
|
45
|
+
CLISSHDelegator.new(@argv, **@opts).run!
|
46
|
+
else
|
47
|
+
send "do_#{subcommand}", @opts
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def working_dir
|
54
|
+
repo_root
|
55
|
+
rescue TinyCI::Subprocesses::SubprocessError => e
|
56
|
+
raise e unless e.message == '`git rev-parse --is-inside-git-dir` failed with code 32768'
|
57
|
+
|
58
|
+
exit 1
|
59
|
+
end
|
60
|
+
|
61
|
+
def global_parser
|
62
|
+
OptionParser.new do |o|
|
20
63
|
o.banner = ''
|
21
|
-
o.on(
|
22
|
-
o.on(
|
64
|
+
o.on('-q', '--[no-]quiet', 'surpress output') { |q| @opts[:quiet] = q }
|
65
|
+
o.on('--running-remotely') { |_rr| @opts[:running_remotely] = true }
|
66
|
+
o.on('-D=DIR', '--dir=DIR', 'specify repository location') { |d| @opts[:working_dir] = d }
|
67
|
+
o.on('-r [REMOTE]', '--remote [REMOTE]',
|
68
|
+
'specify remote') { |r| @opts[:remote] = r.nil? ? true : r }
|
23
69
|
end
|
24
|
-
|
25
|
-
|
70
|
+
end
|
71
|
+
|
72
|
+
def global_help
|
73
|
+
global_parser.help.split("\n").reject do |help_line|
|
74
|
+
HIDDEN_OPTIONS.any? { |o| help_line =~ Regexp.new(o) }
|
75
|
+
end.join("\n").strip
|
76
|
+
end
|
77
|
+
|
78
|
+
def subcommand_banner(subcommand_name)
|
79
|
+
"#{LOGO % TinyCI::VERSION}\nGlobal options:\n #{global_help}\n\n#{subcommand_name} options:"
|
80
|
+
end
|
81
|
+
|
82
|
+
def subcommand_parsers
|
83
|
+
{
|
26
84
|
'run' => OptionParser.new do |o|
|
27
|
-
o.banner =
|
28
|
-
o.on(
|
29
|
-
|
85
|
+
o.banner = subcommand_banner('run')
|
86
|
+
o.on('-c <SHA>', '--commit <SHA>',
|
87
|
+
'run against a specific commit') { |c| @opts[:commit] = c }
|
88
|
+
o.on('-a', '--all',
|
89
|
+
'run against all commits which have not been run against before') { |a| @opts[:all] = a }
|
30
90
|
end,
|
31
91
|
'install' => OptionParser.new do |o|
|
32
|
-
o.banner =
|
33
|
-
o.on(
|
92
|
+
o.banner = subcommand_banner('install')
|
93
|
+
o.on('-a', '--[no-]absolute-path',
|
94
|
+
'install hook with absolute path to specific tinyci version (not recommended)') { |v| @opts[:absolute_path] = v }
|
34
95
|
end,
|
35
96
|
'compact' => OptionParser.new do |o|
|
36
|
-
o.banner =
|
37
|
-
o.on(
|
38
|
-
|
39
|
-
o.on(
|
97
|
+
o.banner = subcommand_banner('compact')
|
98
|
+
o.on('-n', '--num-builds-to-leave <NUM>',
|
99
|
+
'number of builds to leave in place, starting from the most recent') { |n| @opts[:num_builds_to_leave] = n }
|
100
|
+
o.on('-b', '--builds-to-leave <BUILDS>',
|
101
|
+
'specific build directories to leave in place, comma-separated') { |b| @opts[:builds_to_leave] = b.split(',') }
|
102
|
+
end,
|
103
|
+
'log' => OptionParser.new do |o|
|
104
|
+
o.banner = subcommand_banner('log')
|
105
|
+
o.on('-f', '--follow', 'follow the logfile') { |f| @opts[:follow] = f }
|
106
|
+
o.on('-n <NUM>', '--num-lines <NUM>',
|
107
|
+
'number of lines to print') { |n| @opts[:num_lines] = n.to_i }
|
108
|
+
o.on('-c <SHA>', '--commit <SHA>',
|
109
|
+
'run against a specific commit') { |c| @opts[:commit] = c }
|
40
110
|
end
|
41
111
|
}
|
42
|
-
|
43
|
-
banner = <<TXT
|
44
|
-
#{LOGO % TinyCI::VERSION}
|
45
|
-
Global options:
|
46
|
-
#{global.help.strip}
|
47
|
-
|
48
|
-
Available commands:
|
49
|
-
run build and test the repo
|
50
|
-
install install the git hook into the current repository
|
51
|
-
compact compress old build artifacts
|
52
|
-
version print the TinyCI version number
|
53
|
-
TXT
|
54
|
-
if argv[0] == '--help'
|
55
|
-
puts banner
|
56
|
-
return false
|
57
|
-
end
|
58
|
-
|
59
|
-
global.order!(argv)
|
60
|
-
command = argv.shift
|
61
|
-
|
62
|
-
if command.nil? || subcommands[command].nil?
|
63
|
-
puts banner
|
64
|
-
return false
|
65
|
-
end
|
66
|
-
|
67
|
-
subcommands[command].order!(argv)
|
68
|
-
|
69
|
-
opts[:dir] ||= begin
|
70
|
-
repo_root
|
71
|
-
rescue TinyCI::Subprocesses::SubprocessError => e
|
72
|
-
if e.message == '`git rev-parse --is-inside-git-dir` failed with code 32768'
|
73
|
-
exit 1
|
74
|
-
else
|
75
|
-
raise e
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
send "do_#{command}", opts
|
80
112
|
end
|
81
|
-
|
82
|
-
def
|
113
|
+
|
114
|
+
def banner
|
115
|
+
<<~TXT
|
116
|
+
#{LOGO % TinyCI::VERSION}
|
117
|
+
Global options:
|
118
|
+
#{global_help}
|
119
|
+
|
120
|
+
Available commands:
|
121
|
+
run build and test the repo
|
122
|
+
install install the git hook into the current repository
|
123
|
+
compact compress old build artifacts
|
124
|
+
log print logfiles
|
125
|
+
version print the TinyCI version number
|
126
|
+
TXT
|
127
|
+
end
|
128
|
+
|
129
|
+
def subcommand_index
|
130
|
+
@argv.index { |arg| subcommand_parsers.keys.include? arg }
|
131
|
+
end
|
132
|
+
|
133
|
+
def subcommand
|
134
|
+
return nil unless subcommand_index
|
135
|
+
|
136
|
+
@argv[subcommand_index]
|
137
|
+
end
|
138
|
+
|
139
|
+
def global_args
|
140
|
+
@argv[0..subcommand_index - 1]
|
141
|
+
end
|
142
|
+
|
143
|
+
def subcommand_args
|
144
|
+
@argv[subcommand_index + 1..-1]
|
145
|
+
end
|
146
|
+
|
147
|
+
def do_run(opts)
|
83
148
|
if PidFile.running?
|
84
149
|
puts 'TinyCI is already running!' unless opts[:quiet]
|
85
150
|
return false
|
86
151
|
end
|
87
|
-
|
152
|
+
|
88
153
|
opts.delete(:commit) if opts[:all]
|
89
|
-
|
154
|
+
|
90
155
|
if !opts[:commit] && !opts[:all]
|
91
|
-
puts
|
156
|
+
puts 'You must pass either --commit or --all, or try --help' unless opts[:quiet]
|
92
157
|
return false
|
93
158
|
end
|
94
|
-
|
159
|
+
|
95
160
|
logger = MultiLogger.new(quiet: opts[:quiet])
|
96
|
-
result = Scheduler.new(commit: opts[:commit], logger: logger,
|
97
|
-
|
161
|
+
result = Scheduler.new(commit: opts[:commit], logger: logger,
|
162
|
+
working_dir: opts[:working_dir]).run!
|
163
|
+
|
98
164
|
result
|
99
165
|
end
|
100
|
-
|
101
|
-
def
|
166
|
+
|
167
|
+
def do_install(opts)
|
102
168
|
logger = MultiLogger.new(quiet: opts[:quiet])
|
103
|
-
|
104
|
-
TinyCI::Installer.new(logger: logger, working_dir: opts[:
|
169
|
+
|
170
|
+
TinyCI::Installer.new(logger: logger, working_dir: opts[:working_dir],
|
171
|
+
absolute_path: opts[:absolute_path]).install!
|
105
172
|
end
|
106
|
-
|
107
|
-
def
|
173
|
+
|
174
|
+
def do_compact(opts)
|
108
175
|
logger = MultiLogger.new(quiet: opts[:quiet])
|
109
|
-
|
110
|
-
TinyCI::Compactor.new(
|
176
|
+
|
177
|
+
TinyCI::Compactor.new(
|
178
|
+
logger: logger,
|
179
|
+
working_dir: opts[:working_dir],
|
180
|
+
num_builds_to_leave: opts[:num_builds_to_leave],
|
181
|
+
builds_to_leave: opts[:builds_to_leave]
|
182
|
+
).compact!
|
111
183
|
end
|
112
|
-
|
113
|
-
def
|
184
|
+
|
185
|
+
def do_log(opts)
|
186
|
+
TinyCI::LogViewer.new(
|
187
|
+
working_dir: opts[:working_dir],
|
188
|
+
commit: opts[:commit],
|
189
|
+
follow: opts[:follow],
|
190
|
+
num_lines: opts[:num_lines]
|
191
|
+
).view!
|
192
|
+
end
|
193
|
+
|
194
|
+
def do_version(_opts)
|
114
195
|
puts TinyCI::VERSION
|
115
|
-
|
196
|
+
|
116
197
|
true
|
117
198
|
end
|
118
199
|
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tinyci/git_utils'
|
4
|
+
require 'net/ssh'
|
5
|
+
|
6
|
+
module TinyCI
|
7
|
+
# Wrapper that takes calls to {CLI} from the command line and executes them remotely via ssh, on
|
8
|
+
# the server hosting the remote tinyci is installed to. The main purpose is to allow convenient
|
9
|
+
# execution of `tinyci log` on the remote server.
|
10
|
+
class CLISSHDelegator
|
11
|
+
include GitUtils
|
12
|
+
|
13
|
+
#
|
14
|
+
# Constructor
|
15
|
+
#
|
16
|
+
# @param [Array<String>] argv The original arguments passed into {CLI}
|
17
|
+
# @param [String] working_dir The (local) directory from which to run.
|
18
|
+
# @param [String, Boolean] remote Which remote to ssh into. If this is set to `true` then use
|
19
|
+
# the upstream remote for the current branch
|
20
|
+
# @param [<Type>] **opts <description>
|
21
|
+
#
|
22
|
+
def initialize(argv, working_dir:, remote:, **opts)
|
23
|
+
@argv = argv
|
24
|
+
@working_dir = working_dir
|
25
|
+
|
26
|
+
# Handle `remote: true` case where `--remote` switch is passed on its own without specifying a
|
27
|
+
# remote name. The fact of this case must be stored so that the arguments to the remote
|
28
|
+
# execution can be properly constructed.
|
29
|
+
if remote == true
|
30
|
+
@remote = current_tracking_remote
|
31
|
+
@derived_remote = true
|
32
|
+
else
|
33
|
+
@remote = remote
|
34
|
+
@derived_remote = false
|
35
|
+
end
|
36
|
+
@opts = opts
|
37
|
+
end
|
38
|
+
|
39
|
+
def run!
|
40
|
+
unless remote_exists?
|
41
|
+
warn "Remote `#{@remote}` not found"
|
42
|
+
|
43
|
+
return false
|
44
|
+
end
|
45
|
+
|
46
|
+
if github_remote?
|
47
|
+
msg = "`#{@remote}` is a github remote: #{remote_url}"
|
48
|
+
msg += "\nPerhaps you meant to run tinyci #{@argv.first} against a different one?"
|
49
|
+
|
50
|
+
warn msg
|
51
|
+
return false
|
52
|
+
end
|
53
|
+
|
54
|
+
unless ssh_remote?
|
55
|
+
msg = "`#{@remote}` does not appear to have an ssh remote: #{remote_url}"
|
56
|
+
msg += "\nPerhaps you meant to run tinyci #{@argv.first} against a different one?"
|
57
|
+
|
58
|
+
warn msg
|
59
|
+
return false
|
60
|
+
end
|
61
|
+
|
62
|
+
do_tunnel!(remote_url.host, remote_url.user, command)
|
63
|
+
|
64
|
+
true
|
65
|
+
end
|
66
|
+
|
67
|
+
def command
|
68
|
+
(['tinyci', '--running-remotely', '--dir', remote_url.path] + args).join ' '
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# Build the argument list to execute at the remote end.
|
73
|
+
# Main concern here is removing the arguments which are specific to remote execution, ie.
|
74
|
+
# those relevant only to this class.
|
75
|
+
#
|
76
|
+
def args
|
77
|
+
args = @argv.clone
|
78
|
+
|
79
|
+
# since --dir always has an argument we can delete twice to get rid of the switch and the arg
|
80
|
+
index = args.index('--dir') || args.index('-D')
|
81
|
+
2.times { args.delete_at index } if index
|
82
|
+
|
83
|
+
# the --remote switch can live on its own
|
84
|
+
index = args.index('--remote') || args.index('-r')
|
85
|
+
if index
|
86
|
+
args.delete_at index
|
87
|
+
args.delete_at index unless @derived_remote
|
88
|
+
end
|
89
|
+
|
90
|
+
args
|
91
|
+
end
|
92
|
+
|
93
|
+
def do_tunnel!(host, user, cmd)
|
94
|
+
Net::SSH.start(host, user) do |ssh|
|
95
|
+
ssh.open_channel do |ch|
|
96
|
+
ch.exec cmd do |e_ch, success|
|
97
|
+
abort 'could not execute command' unless success
|
98
|
+
e_ch.on_data do |_ch, data|
|
99
|
+
print data
|
100
|
+
end
|
101
|
+
e_ch.on_extended_data do |_ch, _type, data|
|
102
|
+
warn data
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
ssh.loop
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
data/lib/tinyci/compactor.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'fileutils'
|
2
4
|
require 'tinyci/git_utils'
|
3
5
|
require 'zlib'
|
@@ -5,16 +7,16 @@ require 'rubygems/package'
|
|
5
7
|
require 'find'
|
6
8
|
|
7
9
|
module TinyCI
|
8
|
-
|
9
10
|
# Tool for compressing old builds into .tar.gz files
|
10
11
|
class Compactor
|
11
12
|
include GitUtils
|
12
13
|
include Subprocesses
|
13
|
-
|
14
|
+
|
14
15
|
BLOCKSIZE_TO_READ = 1024 * 1000
|
15
|
-
|
16
|
+
|
17
|
+
#
|
16
18
|
# Constructor
|
17
|
-
#
|
19
|
+
#
|
18
20
|
# @param [String] working_dir The directory from which to run.
|
19
21
|
# @param [Integer] num_builds_to_leave How many builds not to compact, starting from the newest
|
20
22
|
# @param [String] builds_to_leave Comma-separated list of build directory names not to compact
|
@@ -23,77 +25,75 @@ module TinyCI
|
|
23
25
|
@logger = logger
|
24
26
|
@working_dir = working_dir || repo_root
|
25
27
|
@num_builds_to_leave = (num_builds_to_leave || 1).to_i
|
26
|
-
@builds_to_leave = builds_to_leave || []
|
28
|
+
@builds_to_leave = (Array(builds_to_leave) || []).map(&:to_s)
|
27
29
|
end
|
28
|
-
|
30
|
+
|
29
31
|
# Compress and delete the build directories
|
30
32
|
def compact!
|
31
33
|
unless inside_repository?
|
32
|
-
log_error
|
34
|
+
log_error 'not currently in a git repository'
|
33
35
|
return false
|
34
36
|
end
|
35
|
-
|
37
|
+
|
36
38
|
directories_to_compact.each do |dir|
|
37
39
|
compress_directory dir
|
38
40
|
FileUtils.rm_rf builds_dir(dir)
|
39
|
-
|
41
|
+
|
40
42
|
log_info "Compacted #{archive_path(dir)}"
|
41
43
|
end
|
42
44
|
end
|
43
|
-
|
45
|
+
|
44
46
|
private
|
45
|
-
|
47
|
+
|
46
48
|
# Build the list of directories to compact according to the options
|
47
49
|
def directories_to_compact
|
48
50
|
builds = Dir.entries builds_dir
|
49
|
-
builds.select! {|e| File.directory? builds_dir(e) }
|
50
|
-
builds.reject! {|e| %w
|
51
|
+
builds.select! { |e| File.directory? builds_dir(e) }
|
52
|
+
builds.reject! { |e| %w[. ..].include? e }
|
51
53
|
builds.sort!
|
52
|
-
|
53
|
-
builds = builds[0..-(@num_builds_to_leave+1)]
|
54
|
-
builds.reject! {|e| @builds_to_leave.include?(e) || @builds_to_leave.include?(builds_dir(e
|
54
|
+
|
55
|
+
builds = builds[0..-(@num_builds_to_leave + 1)]
|
56
|
+
builds.reject! { |e| @builds_to_leave.include?(e) || @builds_to_leave.include?(builds_dir(e)) }
|
55
57
|
|
56
58
|
builds
|
57
59
|
end
|
58
|
-
|
60
|
+
|
59
61
|
# Get the location of the builds directory
|
60
62
|
def builds_dir(*path_segments)
|
61
63
|
File.join @working_dir, 'builds/', *path_segments
|
62
64
|
end
|
63
|
-
|
65
|
+
|
64
66
|
# Build the path for a compressed archive
|
65
67
|
def archive_path(dir)
|
66
|
-
File.join(builds_dir, dir+
|
68
|
+
File.join(builds_dir, dir + '.tar.gz')
|
67
69
|
end
|
68
|
-
|
70
|
+
|
69
71
|
# Create a .tar.gz file from a directory
|
70
72
|
# Done in pure ruby to ensure portability
|
71
73
|
def compress_directory(dir)
|
72
74
|
File.open archive_path(dir), 'wb' do |oarchive_path|
|
73
75
|
Zlib::GzipWriter.wrap oarchive_path do |gz|
|
74
76
|
Gem::Package::TarWriter.new gz do |tar|
|
75
|
-
Find.find "#{builds_dir}/"+dir do |f|
|
76
|
-
relative_path = f.sub "#{builds_dir}/",
|
77
|
+
Find.find "#{builds_dir}/" + dir do |f|
|
78
|
+
relative_path = f.sub "#{builds_dir}/", ''
|
77
79
|
mode = File.stat(f).mode
|
78
80
|
size = File.stat(f).size
|
79
|
-
|
81
|
+
|
80
82
|
if File.directory? f
|
81
83
|
tar.mkdir relative_path, mode
|
82
84
|
else
|
83
85
|
tar.add_file_simple relative_path, mode, size do |tio|
|
84
86
|
File.open f, 'rb' do |rio|
|
85
|
-
while buffer = rio.read(BLOCKSIZE_TO_READ)
|
87
|
+
while (buffer = rio.read(BLOCKSIZE_TO_READ))
|
86
88
|
tio.write buffer
|
87
89
|
end
|
88
90
|
end
|
89
91
|
end
|
90
92
|
end
|
91
|
-
|
92
93
|
end
|
93
94
|
end
|
94
95
|
end
|
95
96
|
end
|
96
|
-
|
97
97
|
end
|
98
98
|
end
|
99
99
|
end
|
data/lib/tinyci/config.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'tinyci/config_transformer'
|
2
4
|
require 'tinyci/symbolize'
|
3
5
|
require 'yaml'
|
@@ -6,60 +8,57 @@ module TinyCI
|
|
6
8
|
# Represents the Configuration for a repo, parsed from the `.tinyci.yml` file in the repo root.
|
7
9
|
# Mainly a wrapper around a the hash object parsed from the yaml in the config file.
|
8
10
|
# The keys of the hash are recursively symbolized.
|
9
|
-
#
|
11
|
+
#
|
10
12
|
# As it is loaded, the configuration file data is passed through the {TinyCI::ConfigTransformer}
|
11
13
|
# class, which translates any definitions in the concise format into the more verbose format
|
12
14
|
class Config
|
13
15
|
include Symbolize
|
14
|
-
|
16
|
+
|
17
|
+
attr_accessor :config_path
|
18
|
+
|
15
19
|
# Constructor
|
16
|
-
#
|
20
|
+
#
|
17
21
|
# @param [String] working_dir The working directory in which to find the config file
|
18
22
|
# @param [String] config_path Override the path to the config file
|
19
23
|
# @param [String] config Override the config content
|
20
|
-
#
|
24
|
+
#
|
21
25
|
# @raise [ConfigMissingError] if the config file is not found
|
22
|
-
def initialize(
|
23
|
-
@
|
24
|
-
@config_pathname = config_path
|
26
|
+
def initialize(config_path, config: nil)
|
27
|
+
@config_path = config_path
|
25
28
|
@config_content = config
|
26
|
-
|
27
|
-
raise ConfigMissingError, "config file #{
|
29
|
+
|
30
|
+
raise ConfigMissingError, "config file #{config_path} not found" unless config_file_exists?
|
28
31
|
end
|
29
|
-
|
32
|
+
|
30
33
|
# Address into the config object
|
31
|
-
#
|
34
|
+
#
|
32
35
|
# @param [Symbol] key The key to address
|
33
36
|
def [](key)
|
34
37
|
config_content[key]
|
35
38
|
end
|
36
|
-
|
39
|
+
|
37
40
|
# Return the raw hash representation
|
38
|
-
#
|
41
|
+
#
|
39
42
|
# @return [Hash] The configuration as a hash
|
40
43
|
def to_hash
|
41
44
|
config_content
|
42
45
|
end
|
43
|
-
|
46
|
+
|
44
47
|
private
|
45
|
-
|
48
|
+
|
46
49
|
def config_file_exists?
|
47
|
-
File.exist?
|
48
|
-
end
|
49
|
-
|
50
|
-
def config_pathname
|
51
|
-
@config_pathname || File.expand_path('.tinyci.yml', @working_dir)
|
50
|
+
File.exist? config_path
|
52
51
|
end
|
53
|
-
|
52
|
+
|
54
53
|
def config_content
|
55
54
|
@config_content ||= begin
|
56
|
-
config = YAML.safe_load(File.read(
|
55
|
+
config = YAML.safe_load(File.read(config_path))
|
57
56
|
transformed_config = ConfigTransformer.new(config).transform!
|
58
57
|
symbolize(transformed_config).freeze
|
59
58
|
end
|
60
59
|
end
|
61
60
|
end
|
62
|
-
|
61
|
+
|
63
62
|
# Error raised when the config file cannot be found
|
64
63
|
class ConfigMissingError < StandardError; end
|
65
64
|
end
|
@@ -1,61 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module TinyCI
|
2
4
|
# Transforms the configuration format from the condensed format to the
|
3
5
|
# more verbose format accepted by the rest of the system
|
4
6
|
class ConfigTransformer
|
5
|
-
|
6
7
|
# Constructor
|
7
|
-
#
|
8
|
+
#
|
8
9
|
# @param [Hash] input The configuration object, in the condensed format
|
9
10
|
def initialize(input)
|
10
11
|
@input = input
|
11
12
|
end
|
12
|
-
|
13
|
+
|
13
14
|
# Transforms the config object
|
14
|
-
#
|
15
|
+
#
|
15
16
|
# @return [Hash] The config object in the verbose form
|
16
17
|
def transform!
|
17
|
-
@input.
|
18
|
+
@input.each_with_object({}) do |(key, value), acc|
|
18
19
|
method_name = "transform_#{key}"
|
19
|
-
|
20
|
+
|
20
21
|
if respond_to? method_name, true
|
21
|
-
|
22
22
|
acc.merge! send(method_name, value)
|
23
23
|
else
|
24
24
|
acc[key] = value
|
25
25
|
end
|
26
|
-
|
27
|
-
acc
|
28
26
|
end
|
29
27
|
end
|
30
|
-
|
28
|
+
|
31
29
|
private
|
32
|
-
|
30
|
+
|
33
31
|
def transform_build(value)
|
34
32
|
{
|
35
33
|
builder: {
|
36
|
-
:
|
34
|
+
class: 'ScriptBuilder',
|
37
35
|
config: {
|
38
36
|
command: value
|
39
37
|
}
|
40
38
|
}
|
41
39
|
}
|
42
40
|
end
|
43
|
-
|
41
|
+
|
44
42
|
def transform_test(value)
|
45
43
|
{
|
46
44
|
tester: {
|
47
|
-
:
|
45
|
+
class: 'ScriptTester',
|
48
46
|
config: {
|
49
47
|
command: value
|
50
48
|
}
|
51
49
|
}
|
52
50
|
}
|
53
51
|
end
|
54
|
-
|
52
|
+
|
55
53
|
def transform_hooks(value)
|
56
54
|
{
|
57
55
|
hooker: {
|
58
|
-
:
|
56
|
+
class: 'ScriptHooker',
|
59
57
|
config: value
|
60
58
|
}
|
61
59
|
}
|