tinyci 0.4.2 → 0.5
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 +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/Dockerfile +3 -2
- data/Gemfile +2 -1
- data/Gemfile.lock +56 -29
- 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 -79
- 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 +23 -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 +29 -22
- metadata +87 -14
data/lib/tinyci/cli.rb
CHANGED
@@ -1,119 +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(
|
34
|
-
|
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 }
|
35
95
|
end,
|
36
96
|
'compact' => OptionParser.new do |o|
|
37
|
-
o.banner =
|
38
|
-
o.on(
|
39
|
-
|
40
|
-
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 }
|
41
110
|
end
|
42
111
|
}
|
43
|
-
|
44
|
-
banner = <<TXT
|
45
|
-
#{LOGO % TinyCI::VERSION}
|
46
|
-
Global options:
|
47
|
-
#{global.help.strip}
|
48
|
-
|
49
|
-
Available commands:
|
50
|
-
run build and test the repo
|
51
|
-
install install the git hook into the current repository
|
52
|
-
compact compress old build artifacts
|
53
|
-
version print the TinyCI version number
|
54
|
-
TXT
|
55
|
-
if argv[0] == '--help'
|
56
|
-
puts banner
|
57
|
-
return false
|
58
|
-
end
|
59
|
-
|
60
|
-
global.order!(argv)
|
61
|
-
command = argv.shift
|
62
|
-
|
63
|
-
if command.nil? || subcommands[command].nil?
|
64
|
-
puts banner
|
65
|
-
return false
|
66
|
-
end
|
67
|
-
|
68
|
-
subcommands[command].order!(argv)
|
69
|
-
|
70
|
-
opts[:dir] ||= begin
|
71
|
-
repo_root
|
72
|
-
rescue TinyCI::Subprocesses::SubprocessError => e
|
73
|
-
if e.message == '`git rev-parse --is-inside-git-dir` failed with code 32768'
|
74
|
-
exit 1
|
75
|
-
else
|
76
|
-
raise e
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
send "do_#{command}", opts
|
81
112
|
end
|
82
|
-
|
83
|
-
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)
|
84
148
|
if PidFile.running?
|
85
149
|
puts 'TinyCI is already running!' unless opts[:quiet]
|
86
150
|
return false
|
87
151
|
end
|
88
|
-
|
152
|
+
|
89
153
|
opts.delete(:commit) if opts[:all]
|
90
|
-
|
154
|
+
|
91
155
|
if !opts[:commit] && !opts[:all]
|
92
|
-
puts
|
156
|
+
puts 'You must pass either --commit or --all, or try --help' unless opts[:quiet]
|
93
157
|
return false
|
94
158
|
end
|
95
|
-
|
159
|
+
|
96
160
|
logger = MultiLogger.new(quiet: opts[:quiet])
|
97
|
-
result = Scheduler.new(commit: opts[:commit], logger: logger,
|
98
|
-
|
161
|
+
result = Scheduler.new(commit: opts[:commit], logger: logger,
|
162
|
+
working_dir: opts[:working_dir]).run!
|
163
|
+
|
99
164
|
result
|
100
165
|
end
|
101
|
-
|
102
|
-
def
|
166
|
+
|
167
|
+
def do_install(opts)
|
103
168
|
logger = MultiLogger.new(quiet: opts[:quiet])
|
104
|
-
|
105
|
-
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!
|
106
172
|
end
|
107
|
-
|
108
|
-
def
|
173
|
+
|
174
|
+
def do_compact(opts)
|
109
175
|
logger = MultiLogger.new(quiet: opts[:quiet])
|
110
|
-
|
111
|
-
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!
|
112
183
|
end
|
113
|
-
|
114
|
-
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)
|
115
195
|
puts TinyCI::VERSION
|
116
|
-
|
196
|
+
|
117
197
|
true
|
118
198
|
end
|
119
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
|