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.
@@ -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
- extend GitUtils
13
-
14
- LOGO = File.read(File.expand_path('logo.txt', __dir__))
15
-
16
- def self.parse!(argv = ARGV)
17
- opts = {}
18
-
19
- global = OptionParser.new do |o|
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("-q", "--[no-]quiet", "surpress output") {|q| opts[:quiet] = q}
22
- o.on("-D <DIR>", "--dir <DIR>", "specify repository location") {|d| opts[:dir] = d}
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
- subcommands = {
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 = "#{LOGO % TinyCI::VERSION}\nGlobal options:\n #{global.help.slice(3..-1)}\nrun options:"
28
- o.on("-c <SHA>", "--commit <SHA>", "run against a specific commit") {|c| opts[:commit] = c}
29
- o.on("-a", "--all", "run against all commits which have not been run against before") {|a| opts[:all] = a}
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 = "Usage: install [options]"
33
- o.on("-q", "--[no-]quiet", "quietly run") {|v| opts[:quiet] = v}
34
- o.on("-a", "--[no-]absolute-path", "install hook with absolute path to specific tinyci version (not recommended)") {|v| opts[:absolute_path] = v}
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 = "Usage: compact [options]"
38
- o.on("-n", "--num-builds-to-leave <NUM>", "number of builds to leave in place, starting from the most recent") {|n| opts[:num_builds_to_leave] = n}
39
- o.on("-b", "--builds-to-leave <BUILDS>", "specific build directories to leave in place, comma-separated") {|b| opts[:builds_to_leave] = b.split(',')}
40
- o.on("-q", "--[no-]quiet", "quietly run") {|v| opts[:quiet] = v}
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 self.do_run(opts)
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 "You must pass either --commit or --all, or try --help" unless opts[:quiet]
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, working_dir: opts[:dir]).run!
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 self.do_install(opts)
166
+
167
+ def do_install(opts)
103
168
  logger = MultiLogger.new(quiet: opts[:quiet])
104
-
105
- TinyCI::Installer.new(logger: logger, working_dir: opts[:dir], absolute_path: opts[:absolute_path]).install!
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 self.do_compact(opts)
173
+
174
+ def do_compact(opts)
109
175
  logger = MultiLogger.new(quiet: opts[:quiet])
110
-
111
- TinyCI::Compactor.new(logger: logger, working_dir: opts[:dir], num_builds_to_leave: opts[:num_builds_to_leave], builds_to_leave: opts[:builds_to_leave]).compact!
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 do_version(opts)
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
@@ -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 "not currently in a git repository"
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{. ..}.include? e }
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, 'export'))}
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+".tar.gz")
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