tinyci 0.4 → 0.5.1

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.
data/lib/tinyci/runner.rb CHANGED
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'tinyci/runner'
2
4
  require 'tinyci/subprocesses'
3
5
  require 'tinyci/git_utils'
@@ -7,82 +9,82 @@ module TinyCI
7
9
  # Manages the execution of test jobs. Responsible for deciding which
8
10
  # commits need to be built and tested. Also manages the pidfile. This
9
11
  # is the main entrypoint for TinyCI.
10
- #
12
+ #
11
13
  # @attr_reader [String] working_dir The working directory to execute against
12
14
  class Scheduler
13
15
  include Subprocesses
14
16
  include Logging
15
17
  include GitUtils
16
-
18
+
17
19
  attr_reader :working_dir
18
-
20
+
19
21
  # Constructor, allows injection of configuration and custom {Runner} class.
20
22
  # Config params are passed to {Runner} instances.
21
- #
23
+ #
22
24
  # @param working_dir [String] The working directory to execute against
23
25
  # @param logger [Logger] Logger object
24
26
  # @param commit [String] specific git object to run against
25
27
  # @param runner_class [TinyCI::Runner] Injection of {Runner} dependency
26
28
  def initialize(
27
- working_dir: nil,
28
- logger: nil,
29
- commit: nil,
30
- runner_class: Runner
31
- )
32
-
29
+ working_dir: nil,
30
+ logger: nil,
31
+ commit: nil,
32
+ runner_class: Runner
33
+ )
34
+
33
35
  @working_dir = working_dir || repo_root
34
36
  @logger = logger
35
37
  @runner_class = runner_class
36
38
  @commit = commit
37
39
  end
38
-
40
+
39
41
  # Runs the TinyCI system against the relevant commits. Also sets up the pidfile.
40
- #
42
+ #
41
43
  # @return [Boolean] `true` if all commits built and tested successfully, `false` otherwise
42
44
  def run!
43
45
  pid = PidFile.new(pidfile: 'tinyci.pid', piddir: @working_dir)
44
-
46
+
45
47
  result = if @commit
46
- run_commit get_commit @commit
47
- else
48
- run_all_commits
49
- end
50
-
48
+ run_commit get_commit @commit
49
+ else
50
+ run_all_commits
51
+ end
52
+
51
53
  pid.release
52
-
54
+
53
55
  result
54
56
  end
55
-
57
+
56
58
  private
57
-
59
+
58
60
  # Git objects to be executed against, all those without a tinyci tag
59
- #
61
+ #
60
62
  # @return [Array<String>] the sha1 hashes in reverse order of creation time
61
- def get_commits
63
+ def retrieve_commits
62
64
  log = execute(git_cmd('log', '--notes=tinyci*', '--format=%H %ct %N§§§', '--reverse'))
63
- lines = log.split("§§§")
64
-
65
- lines.map {|l| format_commit_data(l)}.select {|c| c[:result].nil?}
65
+ lines = log.split('§§§')
66
+
67
+ lines.map { |l| format_commit_data(l) }.select { |c| c[:result].nil? }
66
68
  end
67
-
69
+
68
70
  def get_commit(sha)
69
- data = execute(git_cmd('show', '--quiet', '--notes=tinyci*', "--format=%H %ct", sha))
70
-
71
+ data = execute(git_cmd('show', '--quiet', '--notes=tinyci*', '--format=%H %ct', sha))
72
+
71
73
  format_commit_data(data)
72
74
  end
73
-
75
+
74
76
  # Instantiates {Runner} for a given git object, runs it, and stores the result
75
77
  def run_commit(commit)
76
78
  result = @runner_class.new(
77
- working_dir: @working_dir,
79
+ working_dir: @working_dir,
78
80
  commit: commit[:sha],
79
81
  time: commit[:time],
80
82
  logger: @logger
81
83
  ).run!
82
-
84
+
83
85
  set_result(commit, result)
84
86
  end
85
-
87
+
86
88
  def format_commit_data(data)
87
89
  parts = data.split(' ')
88
90
  {
@@ -91,22 +93,22 @@ module TinyCI
91
93
  result: parts[2]
92
94
  }
93
95
  end
94
-
96
+
95
97
  # Repeatedly gets the list of eligable commits and runs TinyCI against them until there are no more remaining
96
98
  def run_all_commits
97
- commits = get_commits
98
-
99
- until commits.empty? do
100
- commits.each {|c| run_commit(c)}
101
-
102
- commits = get_commits
99
+ commits = retrieve_commits
100
+
101
+ until commits.empty?
102
+ commits.each { |c| run_commit(c) }
103
+
104
+ commits = retrieve_commits
103
105
  end
104
106
  end
105
-
107
+
106
108
  # Stores the result in a git note
107
109
  def set_result(commit, result)
108
110
  result_message = result ? 'success' : 'failure'
109
-
111
+
110
112
  execute git_cmd('notes', '--ref', 'tinyci-result', 'add', '-m', result_message, commit[:sha])
111
113
  end
112
114
  end
@@ -1,44 +1,48 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'open3'
4
+ require 'English'
2
5
  require 'tinyci/logging'
3
6
 
4
7
  module TinyCI
5
8
  # Methods for executing subprocesses in various ways and collecting the results.
6
9
  module Subprocesses
7
-
8
10
  # Synchronously execute a command as a subprocess and return the output.
9
- #
11
+ #
10
12
  # @param [Array<String>] command The command line
11
13
  # @param [String] label A label for debug and logging purposes
12
- #
14
+ #
13
15
  # @return [String] The output of the command
14
16
  # @raise [SubprocessError] if the subprocess returns status > 0
15
17
  def execute(*command, label: nil)
16
- output, status = Open3.capture2(*command.flatten)
17
-
18
+ stdout, stderr, status = Open3.capture3(*command.flatten)
19
+
18
20
  log_debug caller[0]
19
21
  log_debug "CMD: #{command.join(' ')}"
20
- log_debug "OUT: #{output}"
21
-
22
+ log_debug "OUT: #{stdout}"
23
+ log_debug "ERR: #{stderr}"
24
+
22
25
  unless status.success?
23
- log_error output
26
+ log_error stdout
27
+ log_error stderr
24
28
  raise SubprocessError.new(label, command.join(' '), status)
25
29
  end
26
-
27
- output.chomp
30
+
31
+ stdout.chomp
28
32
  end
29
-
33
+
30
34
  # Synchronously execute a chain multiple commands piped into each other as a
31
35
  # subprocess and return the output.
32
- #
36
+ #
33
37
  # @param [Array<Array<String>>] commands The command lines
34
38
  # @param [String] label A label for debug and logging purposes
35
- #
39
+ #
36
40
  # @return [String] The output of the command
37
41
  # @raise [SubprocessError] if the subprocess returns status > 0
38
42
  def execute_pipe(*commands, label: nil)
39
43
  stdout, waiters = Open3.pipeline_r(*commands)
40
44
  output = stdout.read
41
-
45
+
42
46
  waiters.each_with_index do |waiter, i|
43
47
  status = waiter.value
44
48
  unless status.success?
@@ -46,67 +50,72 @@ module TinyCI
46
50
  raise SubprocessError.new(label, commands[i].join(' '), status)
47
51
  end
48
52
  end
49
-
53
+
50
54
  output.chomp
51
55
  end
52
-
56
+
53
57
  # Synchronously execute a command as a subprocess and and stream the output
54
58
  # to `STDOUT`
55
- #
59
+ #
56
60
  # @param [Array<String>] command The command line
57
61
  # @param [String] label A label for debug and logging purposes
58
62
  # @param [String] pwd Optionally specify a different working directory in which to execute the command
59
- #
63
+ #
60
64
  # @return [TrueClass] `true` if the command executed successfully
61
65
  # @raise [SubprocessError] if the subprocess returns status > 0
62
66
  def execute_stream(*command, label: nil, pwd: nil)
63
67
  opts = {}
64
68
  opts[:chdir] = pwd unless pwd.nil?
65
-
69
+
66
70
  log_debug "CMD: #{command.join(' ')}"
67
-
71
+
68
72
  Open3.popen2e(command.join(' '), opts) do |stdin, stdout_and_stderr, wait_thr|
69
73
  stdin.close
70
-
74
+
71
75
  until stdout_and_stderr.closed? || stdout_and_stderr.eof?
72
76
  line = stdout_and_stderr.gets
73
77
  log_info line.chomp
74
78
  $stdout.flush
75
79
  end
76
-
77
- unless wait_thr.value.success?
80
+
81
+ unless wait_thr.value.success?
78
82
  raise SubprocessError.new(label, command.join(' '), wait_thr.value)
79
83
  end
84
+
85
+ ensure
80
86
  stdout_and_stderr.close
81
87
  end
82
-
88
+
83
89
  true
84
90
  end
85
-
91
+
92
+ def execute_and_return_status(command)
93
+ system(*command, out: File::NULL, err: File::NULL)
94
+
95
+ $CHILD_STATUS
96
+ end
97
+
86
98
  # An error raised when any of the {Subprocesses} methods fail
87
- #
99
+ #
88
100
  # @attr_reader [Integer] status The return code of the process
89
101
  # @attr_reader [String] command The command used to spawn the process
90
102
  class SubprocessError < RuntimeError
91
103
  attr_reader :status
92
104
  attr_reader :command
93
-
105
+
94
106
  def initialize(label, command, status, message = "#{label}: `#{command}` failed with status #{status.exitstatus}")
95
107
  @status = status
96
108
  @command = command
97
109
  super(message)
98
110
  end
99
111
  end
100
-
101
- private
102
-
112
+
103
113
  def self.included(base)
104
114
  base.include TinyCI::Logging
105
115
  end
106
-
116
+
107
117
  def self.extended(base)
108
118
  base.extend TinyCI::Logging
109
119
  end
110
-
111
120
  end
112
121
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module TinyCI
2
4
  module Symbolize
3
5
  # recursively make all keys of `hash` into symbols
@@ -7,7 +9,7 @@ module TinyCI
7
9
  hash.each { |key, value| h[key.to_sym] = map_value(value) }
8
10
  end
9
11
  end
10
-
12
+
11
13
  def map_value(thing)
12
14
  case thing
13
15
  when Hash
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'tinyci/executor'
2
4
 
3
5
  module TinyCI
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'tinyci/executor'
2
4
 
3
5
  module TinyCI
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module TinyCI
2
- VERSION = "0.4"
4
+ VERSION = '0.5.1'
3
5
  end
data/lib/yard_plugin.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'redcarpet'
2
4
 
3
5
  # This file fixes the mismatch between how github handles links to other
@@ -27,7 +29,13 @@ class CompatMarkdown
27
29
 
28
30
  def initialize(text)
29
31
  renderer = YARD::Templates::Helpers::HtmlHelper::MDLinkRenderer.new
30
- markdown = Redcarpet::Markdown.new(renderer, no_intra_emphasis: true, gh_blockcode: true, fenced_code_blocks: true, autolink: true)
32
+ opts = {
33
+ no_intra_emphasis: true,
34
+ gh_blockcode: true,
35
+ fenced_code_blocks: true,
36
+ autolink: true
37
+ }
38
+ markdown = Redcarpet::Markdown.new(renderer, opts)
31
39
  @to_html = markdown.render(text)
32
40
  end
33
41
  end