tinyci 0.4.2 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,61 +1,63 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'fileutils'
2
4
  require 'tinyci/git_utils'
3
5
 
4
6
  module TinyCI
5
-
6
7
  # Responsible for writing the git hook file
7
8
  class Installer
8
9
  include GitUtils
9
-
10
+
10
11
  # Constructor
11
- #
12
- # @param [String] working_dir The directory from which to run. Does not have to be the root of the repo
12
+ #
13
+ # @param [String] working_dir The directory from which to run. Does not have to be the
14
+ # root of the repo.
13
15
  # @param [Logger] logger Logger object
14
16
  def initialize(working_dir: nil, logger: nil, absolute_path: false)
15
17
  @logger = logger
16
18
  @working_dir = working_dir || repo_root
17
19
  @absolute_path = absolute_path
18
20
  end
19
-
21
+
20
22
  # Write the hook to the relevant path and make it executable
21
23
  def install!
22
24
  unless inside_repository?
23
- log_error "not currently in a git repository"
25
+ log_error 'not currently in a git repository'
24
26
  return false
25
27
  end
26
-
28
+
27
29
  if hook_exists?
28
- log_error "post-update hook already exists in this repository"
30
+ log_error 'post-update hook already exists in this repository'
29
31
  return false
30
32
  end
31
-
32
- File.open(hook_path, 'a') {|f| f.write hook_content}
33
+
34
+ File.open(hook_path, 'a') { |f| f.write hook_content }
33
35
  FileUtils.chmod('u+x', hook_path)
34
-
36
+
35
37
  log_info 'tinyci post-update hook installed successfully'
36
38
  end
37
-
39
+
38
40
  private
39
-
41
+
40
42
  def hook_exists?
41
43
  File.exist? hook_path
42
44
  end
43
-
45
+
44
46
  def hook_path
45
47
  File.expand_path('hooks/post-update', git_directory_path)
46
48
  end
47
-
49
+
48
50
  def bin_path
49
51
  @absolute_path ? Gem.bin_path('tinyci', 'tinyci') : 'tinyci'
50
52
  end
51
-
53
+
52
54
  def hook_content
53
- <<-EOF
54
- #!/bin/sh
55
- unset GIT_DIR
55
+ <<~HOOK
56
+ #!/bin/sh
57
+ unset GIT_DIR
56
58
 
57
- #{bin_path} run --all
58
- EOF
59
+ #{bin_path} run --all
60
+ HOOK
59
61
  end
60
62
  end
61
63
  end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tinyci/path_utils'
4
+ require 'tinyci/git_utils'
5
+ require 'file-tail'
6
+
7
+ module TinyCI
8
+ # For reviewing the log files created by tinyCI runs. Can print lines from either a specific
9
+ # commit's logfile, or from the global logfile. Has functionality similar to the coreutils `tail`
10
+ # command.
11
+ class LogViewer
12
+ include PathUtils
13
+ include GitUtils
14
+
15
+ #
16
+ # Constructor
17
+ #
18
+ # @param [<Type>] working_dir The directory from which to run.
19
+ # @param [<Type>] commit The commit to run against
20
+ # @param [<Type>] follow After printing, instead of exiting, block and wait for additional data to be appended be the file and print it as it
21
+ # is written. Equivalent to unix `tail -f`
22
+ # @param [<Type>] num_lines How many lines of the file to print, starting from the end.
23
+ # Equivalent to unix `tail -n`
24
+ #
25
+ def initialize(working_dir:, commit: nil, follow: false, num_lines: nil)
26
+ @working_dir = working_dir
27
+ @commit = commit
28
+ @follow = follow
29
+ @num_lines = num_lines
30
+ end
31
+
32
+ def view!
33
+ if @follow
34
+ tail
35
+ else
36
+ dump
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def dump
43
+ unless inside_repository?
44
+ warn 'Error: Not currently inside a git repo, or not on a branch'
45
+ return false
46
+ end
47
+
48
+ unless logfile_exists?
49
+ warn "Error: Logfile does not exist at #{logfile_to_read}"
50
+ warn "Did you mean \e[1mtinyci --remote #{current_tracking_remote} log\e[22m?"
51
+ return false
52
+ end
53
+
54
+ if @num_lines.nil?
55
+ puts File.read(logfile_to_read)
56
+ else
57
+ File.open(logfile_to_read) do |log|
58
+ log.extend File::Tail
59
+ log.return_if_eof = true
60
+ log.backward @num_lines if @num_lines
61
+ log.tail { |line| puts line }
62
+ end
63
+ end
64
+ end
65
+
66
+ def tail
67
+ File.open(logfile_to_read) do |log|
68
+ log.extend(File::Tail)
69
+
70
+ log.backward @num_lines if @num_lines
71
+ log.tail { |line| puts line }
72
+ end
73
+ end
74
+
75
+ def logfile_to_read
76
+ if @commit
77
+ logfile_path
78
+ else
79
+ repo_logfile_path
80
+ end
81
+ end
82
+
83
+ def logfile_exists?
84
+ File.exist? logfile_to_read
85
+ end
86
+ end
87
+ end
@@ -1,16 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'tinyci/multi_logger'
2
4
 
3
5
  module TinyCI
4
6
  # Defines helper instance methods for logging to reduce code verbosity
5
7
  module Logging
6
-
7
- %w(log debug info warn error fatal unknown).each do |m|
8
+ %w[log debug info warn error fatal unknown].each do |m|
8
9
  define_method("log_#{m}") do |*args|
9
10
  return false unless defined?(@logger) && @logger.is_a?(MultiLogger)
10
-
11
+
11
12
  @logger.send(m, *args)
12
13
  end
13
- end
14
-
14
+ end
15
15
  end
16
16
  end
@@ -1,48 +1,57 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'logger'
4
+ require 'fileutils'
2
5
 
3
6
  module TinyCI
4
7
  # This class allows logging to both `STDOUT` and to a file with a single call.
5
8
  # @attr [Boolean] quiet Disables logging to STDOUT
6
9
  class MultiLogger
7
- FORMAT = Proc.new do |severity, datetime, progname, msg|
8
- "[#{datetime.strftime "%T"}] #{msg}\n"
10
+ FORMAT = proc do |_severity, datetime, _progname, msg|
11
+ "[#{datetime.strftime '%T'}] #{msg}\n"
9
12
  end
10
-
13
+
11
14
  LEVEL = Logger::INFO
12
-
15
+
13
16
  attr_accessor :quiet
14
-
17
+
15
18
  # Constructor
16
- #
19
+ #
17
20
  # @param [Boolean] quiet Disables logging to STDOUT
18
21
  # @param [String] path Location to write logfile to
19
- def initialize(quiet: false, path: nil)
20
- @file_logger = nil
21
- self.output_path = path
22
+ def initialize(quiet: false, path: nil, paths: [])
23
+ @file_loggers = []
24
+ add_output_path path
25
+ paths.each { |p| add_output_path(p) }
22
26
  @quiet = quiet
23
-
27
+
24
28
  @stdout_logger = Logger.new($stdout)
25
29
  @stdout_logger.formatter = FORMAT
26
30
  @stdout_logger.level = LEVEL
27
31
  end
28
-
32
+
29
33
  def targets
30
34
  logs = []
31
- logs << @file_logger if @file_logger
35
+ logs += @file_loggers
32
36
  logs << @stdout_logger unless @quiet
33
-
37
+
34
38
  logs
35
39
  end
36
-
37
- def output_path=(path)
38
- if path
39
- @file_logger = Logger.new(path)
40
- @file_logger.formatter = FORMAT
41
- @file_logger.level = LEVEL
42
- end
40
+
41
+ def add_output_path(path)
42
+ return unless path
43
+
44
+ FileUtils.touch path
45
+
46
+ logger = Logger.new(path)
47
+ logger.formatter = FORMAT
48
+ logger.level = LEVEL
49
+ @file_loggers << logger
50
+
51
+ logger
43
52
  end
44
53
 
45
- %w(log debug info warn error fatal unknown).each do |m|
54
+ %w[log debug info warn error fatal unknown].each do |m|
46
55
  define_method(m) do |*args|
47
56
  targets.each { |t| t.send(m, *args) }
48
57
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tinyci/subprocesses'
4
+ require 'tinyci/git_utils'
5
+ require 'fileutils'
6
+
7
+ module TinyCI
8
+ # Methods for computing paths.
9
+ module PathUtils
10
+ def builds_path
11
+ File.absolute_path("#{@working_dir}/builds")
12
+ end
13
+
14
+ # Build the absolute target path
15
+ def target_path
16
+ File.join(builds_path, "#{time.to_i}_#{@commit}")
17
+ end
18
+
19
+ # Build the export path
20
+ def export_path
21
+ File.join(target_path, 'export')
22
+ end
23
+
24
+ private
25
+
26
+ def logfile_path
27
+ File.join(target_path, 'tinyci.log')
28
+ end
29
+
30
+ def repo_logfile_path
31
+ File.join(builds_path, 'tinyci.log')
32
+ end
33
+
34
+ # Ensure a path exists
35
+ def ensure_path(path)
36
+ FileUtils.mkdir_p path
37
+ end
38
+
39
+ def self.included(base)
40
+ base.include TinyCI::Subprocesses
41
+ base.include TinyCI::GitUtils
42
+ end
43
+ end
44
+ end
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'tinyci/subprocesses'
2
4
  require 'tinyci/git_utils'
5
+ require 'tinyci/path_utils'
3
6
  require 'tinyci/logging'
4
7
  require 'tinyci/config'
5
8
 
@@ -14,178 +17,191 @@ require 'fileutils'
14
17
 
15
18
  module TinyCI
16
19
  # Responsible for managing the running of TinyCI against a single git object.
17
- #
18
- # @attr builder [TinyCI::Executor] Returns the Builder object. Used solely for testing at this time.
19
- # @attr tester [TinyCI::Executor] Returns the Tester object. Used solely for testing at this time.
20
+ #
21
+ # @attr builder [TinyCI::Executor] Returns the Builder object. Used solely for testing.
22
+ # @attr tester [TinyCI::Executor] Returns the Tester object. Used solely for testing.
20
23
  class Runner
21
24
  include Subprocesses
22
25
  include GitUtils
26
+ include PathUtils
23
27
  include Logging
24
-
28
+
29
+ CONFIG_FILENAME = '.tinyci.yml'
30
+
25
31
  attr_accessor :builder, :tester, :hooker
26
-
32
+
27
33
  # Constructor, allows injection of generic configuration params.
28
- #
34
+ #
29
35
  # @param working_dir [String] The working directory to execute against.
30
36
  # @param commit [String] SHA1 of git object to run against
31
37
  # @param logger [Logger] Logger object
32
- # @param time [Time] Override time of object creation. Used solely for testing at this time.
33
- # @param config [Hash] Override TinyCI config object, normally loaded from `.tinyci` file. Used solely for testing at this time.
38
+ # @param time [Time] Override time of object creation. Used solely for testing.
39
+ # @param config [Hash] Override TinyCI config object, normally loaded from `.tinyci` file.
40
+ # Used solely for testing.
34
41
  def initialize(working_dir: '.', commit:, time: nil, logger: nil, config: nil)
35
42
  @working_dir = working_dir
36
- @logger = logger
37
43
  @config = config
38
44
  @commit = commit
39
- @time = time || commit_time
45
+ @time = time
46
+ @logger = logger.is_a?(MultiLogger) ? MultiLogger.new(quiet: logger.quiet) : nil
47
+ ensure_path target_path
48
+ setup_log
40
49
  end
41
-
50
+
42
51
  # Runs the TinyCI system against the single git object referenced in `@commit`.
43
- #
52
+ #
44
53
  # @return [Boolean] `true` if the commit was built and tested successfully, `false` otherwise
54
+ # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/CyclomaticComplexity
45
55
  def run!
46
56
  begin
47
- ensure_path target_path
48
- setup_log
49
-
50
57
  log_info "Commit: #{@commit}"
51
-
52
- log_info "Cleaning..."
58
+
59
+ unless config_exists?
60
+ log_error "No config found for #{@commit}"
61
+
62
+ return false
63
+ end
64
+
65
+ log_info 'Cleaning...'
53
66
  clean
54
-
55
- log_info "Exporting..."
67
+
68
+ log_info 'Exporting...'
56
69
  ensure_path export_path
57
70
  export
58
-
71
+
59
72
  begin
60
- load_config
73
+ config
61
74
  rescue ConfigMissingError => e
62
75
  log_error e.message
63
76
  log_error 'Removing export...'
64
77
  clean
65
-
78
+
66
79
  return false
67
80
  end
68
81
  @builder ||= instantiate_builder
69
82
  @tester ||= instantiate_tester
70
83
  @hooker ||= instantiate_hooker
71
-
72
- log_info "Building..."
84
+
85
+ log_info 'Building...'
73
86
  run_hook! :before_build
74
87
  begin
75
88
  @builder.build
76
- rescue => e
89
+ rescue StandardError => e
77
90
  run_hook! :after_build_failure
78
-
91
+
79
92
  raise e if ENV['TINYCI_ENV'] == 'test'
80
-
93
+
81
94
  log_error e
82
95
  log_debug e.backtrace
83
-
96
+
84
97
  return false
85
98
  else
86
99
  run_hook! :after_build_success
87
100
  ensure
88
101
  run_hook! :after_build
89
102
  end
90
-
91
-
92
- log_info "Testing..."
103
+
104
+ log_info 'Testing...'
93
105
  run_hook! :before_test
94
106
  begin
95
107
  @tester.test
96
- rescue => e
108
+ rescue StandardError => e
97
109
  run_hook! :after_test_failure
98
-
110
+
99
111
  raise e if ENV['TINYCI_ENV'] == 'test'
100
-
112
+
101
113
  log_error e
102
114
  log_debug e.backtrace
103
-
115
+
104
116
  return false
105
117
  else
106
118
  run_hook! :after_test_success
107
119
  ensure
108
120
  run_hook! :after_test
109
121
  end
110
-
122
+
111
123
  log_info "Finished #{@commit}"
112
- rescue => e
124
+ rescue StandardError => e
113
125
  raise e if ENV['TINYCI_ENV'] == 'test'
114
-
126
+
115
127
  log_error e
116
128
  log_debug e.backtrace
117
129
  return false
118
130
  ensure
119
131
  run_hook! :after_all
120
132
  end
121
-
133
+
122
134
  true
123
135
  end
124
-
125
- # Build the absolute target path
126
- def target_path
127
- File.absolute_path("#{@working_dir}/builds/#{@time.to_i}_#{@commit}/")
128
- end
129
-
130
- # Build the export path
131
- def export_path
132
- File.join(target_path, 'export')
133
- end
134
-
136
+ # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/CyclomaticComplexity
137
+
135
138
  private
136
-
139
+
137
140
  def run_hook!(name)
138
141
  return unless @hooker
139
-
142
+
140
143
  @hooker.send("#{name}!")
141
144
  end
142
-
145
+
143
146
  # Creates log file if it doesnt exist
144
147
  def setup_log
145
148
  return unless @logger.is_a? MultiLogger
146
- FileUtils.touch logfile_path
147
- @logger.output_path = logfile_path
148
- end
149
-
150
- def logfile_path
151
- File.join(target_path, 'tinyci.log')
149
+
150
+ @logger.add_output_path logfile_path
151
+ @logger.add_output_path repo_logfile_path
152
152
  end
153
-
153
+
154
154
  # Instantiate the Builder object according to the class named in the config
155
155
  def instantiate_builder
156
156
  klass = TinyCI::Builders.const_get(@config[:builder][:class])
157
- klass.new(@config[:builder][:config].merge(target: target_path, export: export_path, commit: @commit), logger: @logger)
157
+ konfig = @config[:builder][:config].merge(
158
+ target: target_path,
159
+ export: export_path,
160
+ commit: @commit,
161
+ logger: @logger
162
+ )
163
+ klass.new(konfig)
158
164
  end
159
-
165
+
160
166
  # Instantiate the Tester object according to the class named in the config
161
167
  def instantiate_tester
162
168
  klass = TinyCI::Testers.const_get(@config[:tester][:class])
163
- klass.new(@config[:tester][:config].merge(target: target_path, export: export_path, commit: @commit), logger: @logger)
169
+ konfig = @config[:tester][:config].merge(
170
+ target: target_path,
171
+ export: export_path,
172
+ commit: @commit,
173
+ logger: @logger
174
+ )
175
+ klass.new(konfig)
164
176
  end
165
-
177
+
166
178
  # Instantiate the Hooker object according to the class named in the config
167
179
  def instantiate_hooker
168
180
  return nil unless @config[:hooker].is_a? Hash
169
-
181
+
170
182
  klass = TinyCI::Hookers.const_get(@config[:hooker][:class])
171
- klass.new(@config[:hooker][:config].merge(target: target_path, export: export_path, commit: @commit), logger: @logger)
183
+ konfig = @config[:hooker][:config].merge(
184
+ target: target_path,
185
+ export: export_path,
186
+ commit: @commit,
187
+ logger: @logger
188
+ )
189
+ klass.new(konfig)
172
190
  end
173
-
174
- # Instantiate the {Config} object from the `.tinyci.yml` file in the exported directory
175
- def load_config
176
- @config ||= Config.new(working_dir: export_path)
191
+
192
+ def config_path
193
+ File.expand_path(CONFIG_FILENAME, export_path)
177
194
  end
178
195
 
179
- # Parse the commit time from git
180
- def commit_time
181
- Time.at execute(git_cmd('show', '-s', '--format=%ct', @commit)).to_i
196
+ def config_exists?
197
+ file_exists_in_git? CONFIG_FILENAME
182
198
  end
183
199
 
184
- # Ensure a path exists
185
- def ensure_path(path)
186
- execute 'mkdir', '-p', path
200
+ # The {Config} object from the `.tinyci.yml` file in the exported directory
201
+ def config
202
+ @config ||= Config.new(config_path)
187
203
  end
188
-
204
+
189
205
  # Delete the export path
190
206
  def clean
191
207
  FileUtils.rm_rf export_path
@@ -196,7 +212,8 @@ module TinyCI
196
212
  # a `git export` subcommand.
197
213
  # see https://stackoverflow.com/a/163769
198
214
  def export
199
- execute_pipe git_cmd('archive', '--format=tar', @commit), ['tar', '-C', export_path, '-xf', '-']
215
+ execute_pipe git_cmd('archive', '--format=tar',
216
+ @commit), ['tar', '-C', export_path, '-xf', '-']
200
217
  end
201
218
  end
202
219
  end