gitlab-build-output 0.0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a9865235d35cdd14d7ae16dd29f567e5fb3ba500
4
+ data.tar.gz: 69bd5296f6b8180bb4afaf95253763b743bd3a24
5
+ SHA512:
6
+ metadata.gz: 6d2d5528476051550754782fb7dbd477c34730935e6cbfa7b458f25f77d1bcad670daae1eaf96087a13fc8fd2b8cbab47244866c1a1305ddb594c06f84692482
7
+ data.tar.gz: bd93842b93f28ef57c7d724accde9f1858e0250c62a53435b7d1c38216632431fb163e56d6aea847c9a528a22aee506d516f4d7b154c1a36fc9139f1834dce1b
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,12 @@
1
+ AllCops:
2
+ Exclude:
3
+ - lib/gitlab_build_output/core_ext/**/*
4
+ - lib/gitlab_build_output/ansi2html.rb
5
+
6
+ Style/Documentation:
7
+ Enabled: false
8
+
9
+ Metrics/BlockLength:
10
+ Exclude:
11
+ - spec/**/*_spec.rb
12
+ - gitlab-build-output.gemspec
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ gitlab-build-output
data/.rvm-gemset ADDED
@@ -0,0 +1 @@
1
+ gitlab-build-output
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.0.0
5
+ before_install: gem install bundler -v 1.14.6
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ ruby '2.0.0'
4
+
5
+ # Specify your gem's dependencies in gitlab-build-output.gemspec
6
+ gemspec
data/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # Gitlab::Build::Output
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/gitlab/build/output`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'gitlab-build-output'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install gitlab-build-output
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/gitlab-build-output.
36
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'gitlab/build/output'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'gitlab_build_output'
5
+
6
+ GitLabBuildOutput::Application.start
@@ -0,0 +1,46 @@
1
+ # coding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'gitlab_build_output/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'gitlab-build-output'
9
+ spec.version = GitLabBuildOutput::VERSION
10
+ spec.authors = ['Jacob Carlborg']
11
+ spec.email = ['doob@me.com']
12
+
13
+ spec.summary = 'Prints the output of a GitLab CI job.'
14
+ spec.description = 'Prints the output of a GitLab CI job.'
15
+ spec.homepage = 'https://github.com/jacob-carlborg/gitlab-build-output'
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the
18
+ # 'allowed_push_host' to allow pushing to a single host or delete this section
19
+ # to allow pushing to any host.
20
+ if spec.respond_to?(:metadata)
21
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
22
+ else
23
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
24
+ 'public gem pushes.'
25
+ end
26
+
27
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
28
+ f.match(%r{^(test|spec|features)/})
29
+ end
30
+ spec.bindir = 'exe'
31
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ['lib']
33
+
34
+ spec.add_dependency 'git', '~> 1.3'
35
+ spec.add_dependency 'gitlab', '~> 4.0'
36
+ spec.add_dependency 'git_clone_url', '~> 2.0'
37
+
38
+ spec.add_development_dependency 'bundler', '~> 1.14'
39
+ spec.add_development_dependency 'rake', '~> 10.0'
40
+ spec.add_development_dependency 'rspec', '~> 3.0'
41
+ spec.add_development_dependency 'rubocop', '0.48.1'
42
+ spec.add_development_dependency 'pry', '~> 0.11'
43
+ # spec.add_development_dependency 'pry-byebug', '~> 3.5'
44
+ spec.add_development_dependency 'pry-rescue', '~> 1.4'
45
+ spec.add_development_dependency 'pry-stack_explorer', '~> 0.4.9'
46
+ end
@@ -0,0 +1 @@
1
+ require_relative '../../gitlab_build_output'
@@ -0,0 +1,345 @@
1
+ # Stolen from GitLab https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/ansi2html.rb
2
+ # ANSI color library
3
+ #
4
+ # Implementation per http://en.wikipedia.org/wiki/ANSI_escape_code
5
+ module GitLabBuildOutput
6
+ module Ansi2html
7
+ TRACE_SECTION_REGEX = /section_((?:start)|(?:end)):(\d+):([^\r]+)\r\033\[0K/.freeze
8
+
9
+ # keys represent the trailing digit in color changing command (30-37, 40-47, 90-97. 100-107)
10
+ COLOR = {
11
+ 0 => 'black', # not that this is gray in the intense color table
12
+ 1 => 'red',
13
+ 2 => 'green',
14
+ 3 => 'yellow',
15
+ 4 => 'blue',
16
+ 5 => 'magenta',
17
+ 6 => 'cyan',
18
+ 7 => 'white', # not that this is gray in the dark (aka default) color table
19
+ }.freeze
20
+
21
+ STYLE_SWITCHES = {
22
+ bold: 0x01,
23
+ italic: 0x02,
24
+ underline: 0x04,
25
+ conceal: 0x08,
26
+ cross: 0x10
27
+ }.freeze
28
+
29
+ def self.convert(ansi, state = nil)
30
+ Converter.new.convert(ansi, state)
31
+ end
32
+
33
+ class Converter
34
+ def on_0(s) reset() end
35
+
36
+ def on_1(s) enable(STYLE_SWITCHES[:bold]) end
37
+
38
+ def on_3(s) enable(STYLE_SWITCHES[:italic]) end
39
+
40
+ def on_4(s) enable(STYLE_SWITCHES[:underline]) end
41
+
42
+ def on_8(s) enable(STYLE_SWITCHES[:conceal]) end
43
+
44
+ def on_9(s) enable(STYLE_SWITCHES[:cross]) end
45
+
46
+ def on_21(s) disable(STYLE_SWITCHES[:bold]) end
47
+
48
+ def on_22(s) disable(STYLE_SWITCHES[:bold]) end
49
+
50
+ def on_23(s) disable(STYLE_SWITCHES[:italic]) end
51
+
52
+ def on_24(s) disable(STYLE_SWITCHES[:underline]) end
53
+
54
+ def on_28(s) disable(STYLE_SWITCHES[:conceal]) end
55
+
56
+ def on_29(s) disable(STYLE_SWITCHES[:cross]) end
57
+
58
+ def on_30(s) set_fg_color(0) end
59
+
60
+ def on_31(s) set_fg_color(1) end
61
+
62
+ def on_32(s) set_fg_color(2) end
63
+
64
+ def on_33(s) set_fg_color(3) end
65
+
66
+ def on_34(s) set_fg_color(4) end
67
+
68
+ def on_35(s) set_fg_color(5) end
69
+
70
+ def on_36(s) set_fg_color(6) end
71
+
72
+ def on_37(s) set_fg_color(7) end
73
+
74
+ def on_38(s) set_fg_color_256(s) end
75
+
76
+ def on_39(s) set_fg_color(9) end
77
+
78
+ def on_40(s) set_bg_color(0) end
79
+
80
+ def on_41(s) set_bg_color(1) end
81
+
82
+ def on_42(s) set_bg_color(2) end
83
+
84
+ def on_43(s) set_bg_color(3) end
85
+
86
+ def on_44(s) set_bg_color(4) end
87
+
88
+ def on_45(s) set_bg_color(5) end
89
+
90
+ def on_46(s) set_bg_color(6) end
91
+
92
+ def on_47(s) set_bg_color(7) end
93
+
94
+ def on_48(s) set_bg_color_256(s) end
95
+
96
+ def on_49(s) set_bg_color(9) end
97
+
98
+ def on_90(s) set_fg_color(0, 'l') end
99
+
100
+ def on_91(s) set_fg_color(1, 'l') end
101
+
102
+ def on_92(s) set_fg_color(2, 'l') end
103
+
104
+ def on_93(s) set_fg_color(3, 'l') end
105
+
106
+ def on_94(s) set_fg_color(4, 'l') end
107
+
108
+ def on_95(s) set_fg_color(5, 'l') end
109
+
110
+ def on_96(s) set_fg_color(6, 'l') end
111
+
112
+ def on_97(s) set_fg_color(7, 'l') end
113
+
114
+ def on_99(s) set_fg_color(9, 'l') end
115
+
116
+ def on_100(s) set_bg_color(0, 'l') end
117
+
118
+ def on_101(s) set_bg_color(1, 'l') end
119
+
120
+ def on_102(s) set_bg_color(2, 'l') end
121
+
122
+ def on_103(s) set_bg_color(3, 'l') end
123
+
124
+ def on_104(s) set_bg_color(4, 'l') end
125
+
126
+ def on_105(s) set_bg_color(5, 'l') end
127
+
128
+ def on_106(s) set_bg_color(6, 'l') end
129
+
130
+ def on_107(s) set_bg_color(7, 'l') end
131
+
132
+ def on_109(s) set_bg_color(9, 'l') end
133
+
134
+ attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask
135
+
136
+ STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask].freeze
137
+
138
+ def convert(stream, new_state)
139
+ reset_state
140
+ restore_state(new_state, stream) if new_state.present?
141
+
142
+ append = false
143
+ truncated = false
144
+
145
+ cur_offset = stream.tell
146
+ if cur_offset > @offset
147
+ @offset = cur_offset
148
+ truncated = true
149
+ else
150
+ stream.seek(@offset)
151
+ append = @offset > 0
152
+ end
153
+ start_offset = @offset
154
+
155
+ open_new_tag
156
+
157
+ stream.each_line do |line|
158
+ s = StringScanner.new(line)
159
+ until s.eos?
160
+ if s.scan(TRACE_SECTION_REGEX)
161
+ handle_section(s)
162
+ elsif s.scan(/\e([@-_])(.*?)([@-~])/)
163
+ handle_sequence(s)
164
+ elsif s.scan(/\e(([@-_])(.*?)?)?$/)
165
+ break
166
+ elsif s.scan(/</)
167
+ @out << '&lt;'
168
+ elsif s.scan(/\r?\n/)
169
+ @out << '<br>'
170
+ else
171
+ @out << s.scan(/./m)
172
+ end
173
+ @offset += s.matched_size
174
+ end
175
+ end
176
+
177
+ close_open_tags()
178
+
179
+ OpenStruct.new(
180
+ html: @out.force_encoding(Encoding.default_external),
181
+ state: state,
182
+ append: append,
183
+ truncated: truncated,
184
+ offset: start_offset,
185
+ size: stream.tell - start_offset,
186
+ total: stream.size
187
+ )
188
+ end
189
+
190
+ def handle_section(s)
191
+ action = s[1]
192
+ timestamp = s[2]
193
+ section = s[3]
194
+ line = s.matched()[0...-5] # strips \r\033[0K
195
+
196
+ @out << %{<div class="hidden" data-action="#{action}" data-timestamp="#{timestamp}" data-section="#{section}">#{line}</div>}
197
+ end
198
+
199
+ def handle_sequence(s)
200
+ indicator = s[1]
201
+ commands = s[2].split ';'
202
+ terminator = s[3]
203
+
204
+ # We are only interested in color and text style changes - triggered by
205
+ # sequences starting with '\e[' and ending with 'm'. Any other control
206
+ # sequence gets stripped (including stuff like "delete last line")
207
+ return unless indicator == '[' && terminator == 'm'
208
+
209
+ close_open_tags()
210
+
211
+ if commands.empty?()
212
+ reset()
213
+ return
214
+ end
215
+
216
+ evaluate_command_stack(commands)
217
+
218
+ open_new_tag
219
+ end
220
+
221
+ def evaluate_command_stack(stack)
222
+ return unless command = stack.shift()
223
+
224
+ if self.respond_to?("on_#{command}", true)
225
+ self.__send__("on_#{command}", stack) # rubocop:disable GitlabSecurity/PublicSend
226
+ end
227
+
228
+ evaluate_command_stack(stack)
229
+ end
230
+
231
+ def open_new_tag
232
+ css_classes = []
233
+
234
+ unless @fg_color.nil?
235
+ fg_color = @fg_color
236
+ # Most terminals show bold colored text in the light color variant
237
+ # Let's mimic that here
238
+ if @style_mask & STYLE_SWITCHES[:bold] != 0
239
+ fg_color.sub!(/fg-(\w{2,}+)/, 'fg-l-\1')
240
+ end
241
+ css_classes << fg_color
242
+ end
243
+ css_classes << @bg_color unless @bg_color.nil?
244
+
245
+ STYLE_SWITCHES.each do |css_class, flag|
246
+ css_classes << "term-#{css_class}" if @style_mask & flag != 0
247
+ end
248
+
249
+ return if css_classes.empty?
250
+
251
+ @out << %{<span class="#{css_classes.join(' ')}">}
252
+ @n_open_tags += 1
253
+ end
254
+
255
+ def close_open_tags
256
+ while @n_open_tags > 0
257
+ @out << %{</span>}
258
+ @n_open_tags -= 1
259
+ end
260
+ end
261
+
262
+ def reset_state
263
+ @offset = 0
264
+ @n_open_tags = 0
265
+ @out = ''
266
+ reset
267
+ end
268
+
269
+ def state
270
+ state = STATE_PARAMS.inject({}) do |h, param|
271
+ h[param] = send(param) # rubocop:disable GitlabSecurity/PublicSend
272
+ h
273
+ end
274
+ Base64.urlsafe_encode64(state.to_json)
275
+ end
276
+
277
+ def restore_state(new_state, stream)
278
+ state = Base64.urlsafe_decode64(new_state)
279
+ state = JSON.parse(state, symbolize_names: true)
280
+ return if state[:offset].to_i > stream.size
281
+
282
+ STATE_PARAMS.each do |param|
283
+ send("#{param}=".to_sym, state[param]) # rubocop:disable GitlabSecurity/PublicSend
284
+ end
285
+ end
286
+
287
+ def reset
288
+ @fg_color = nil
289
+ @bg_color = nil
290
+ @style_mask = 0
291
+ end
292
+
293
+ def enable(flag)
294
+ @style_mask |= flag
295
+ end
296
+
297
+ def disable(flag)
298
+ @style_mask &= ~flag
299
+ end
300
+
301
+ def set_fg_color(color_index, prefix = nil)
302
+ @fg_color = get_term_color_class(color_index, ["fg", prefix])
303
+ end
304
+
305
+ def set_bg_color(color_index, prefix = nil)
306
+ @bg_color = get_term_color_class(color_index, ["bg", prefix])
307
+ end
308
+
309
+ def get_term_color_class(color_index, prefix)
310
+ color_name = COLOR[color_index]
311
+ return nil if color_name.nil?
312
+
313
+ get_color_class(["term", prefix, color_name])
314
+ end
315
+
316
+ def set_fg_color_256(command_stack)
317
+ css_class = get_xterm_color_class(command_stack, "fg")
318
+ @fg_color = css_class unless css_class.nil?
319
+ end
320
+
321
+ def set_bg_color_256(command_stack)
322
+ css_class = get_xterm_color_class(command_stack, "bg")
323
+ @bg_color = css_class unless css_class.nil?
324
+ end
325
+
326
+ def get_xterm_color_class(command_stack, prefix)
327
+ # the 38 and 48 commands have to be followed by "5" and the color index
328
+ return unless command_stack.length >= 2
329
+ return unless command_stack[0] == "5"
330
+
331
+ command_stack.shift() # ignore the "5" command
332
+ color_index = command_stack.shift().to_i
333
+
334
+ return unless color_index >= 0
335
+ return unless color_index <= 255
336
+
337
+ get_color_class(["xterm", prefix, color_index])
338
+ end
339
+
340
+ def get_color_class(segments)
341
+ [segments].flatten.compact.join('-')
342
+ end
343
+ end
344
+ end
345
+ end
@@ -0,0 +1,156 @@
1
+ module GitLabBuildOutput
2
+ # rubocop:disable Metrics/ClassLength
3
+ class Application
4
+ attr_reader :raw_args
5
+ attr_reader :args
6
+ attr_reader :option_parser
7
+
8
+ Args = Struct.new(
9
+ :help, :verbose, :version, :private_token, :endpoint, :loop, :html
10
+ )
11
+
12
+ def initialize(raw_args)
13
+ @raw_args = raw_args
14
+ @args = Args.new
15
+ end
16
+
17
+ def self.start
18
+ new(ARGV).run
19
+ end
20
+
21
+ def run
22
+ parse_verbose_argument(raw_args, args)
23
+ handle_errors do
24
+ @option_parser = parse_arguments(raw_args, args)
25
+ exit = handle_arguments(args)
26
+ return if exit
27
+ runner.run
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def runner
34
+ @runner ||=
35
+ (args.loop ? LoopRunner : SingleRunner).new(job_tracer, outputter, 0.5)
36
+ end
37
+
38
+ def job_tracer
39
+ @job_tracer ||= JobTracer.new(
40
+ args.private_token, git_repository, args.endpoint
41
+ )
42
+ end
43
+
44
+ def outputter
45
+ @outputter ||= args.html ? HtmlOutputter.new : Outputter.new
46
+ end
47
+
48
+ def git_repository
49
+ raw_args.first
50
+ end
51
+
52
+ def error_handler
53
+ args.verbose ? :verbose_error_handler : :default_error_handler
54
+ end
55
+
56
+ def handle_errors(&block)
57
+ send(error_handler, &block)
58
+ end
59
+
60
+ def default_error_handler(&block)
61
+ verbose_error_handler(&block)
62
+ rescue OptionParser::InvalidArgument => e
63
+ args = e.args.join(' ')
64
+ puts "Invalid argument: #{args}"
65
+ exit 1
66
+ # rubocop:disable Lint/RescueException
67
+ rescue Exception => e
68
+ # rubocop:enable Lint/RescueException
69
+ puts "An unexpected error occurred: #{e.message}"
70
+ exit 1
71
+ end
72
+
73
+ def verbose_error_handler
74
+ yield
75
+ end
76
+
77
+ def valid_formats_description
78
+ @valid_formats_description ||= begin
79
+ list = VALID_FORMATS.join(', ')
80
+ "(#{list})"
81
+ end
82
+ end
83
+
84
+ def parse_verbose_argument(raw_args, args)
85
+ args.verbose = raw_args.include?('-v') || raw_args.include?('--verbose')
86
+ end
87
+
88
+ # rubocop:disable Metrics/AbcSize
89
+ # rubocop:disable Metrics/MethodLength
90
+ def parse_arguments(raw_args, args)
91
+ opts = OptionParser.new
92
+ opts.banner = banner
93
+ opts.separator ''
94
+ opts.separator 'Options:'
95
+
96
+ opts.on('-e', '--endpoint <endpoint>', 'The GitLab endpoint') do |e|
97
+ args.endpoint = e
98
+ end
99
+
100
+ opts.on('-p', '--private_token <private_token>', 'The private token ' \
101
+ 'used to authenticate to GitLab') do |value|
102
+ args.private_token = value
103
+ end
104
+
105
+ opts.on('-l', '--[no-]loop', 'Loop until the job is complete') do |value|
106
+ args.loop = value
107
+ end
108
+
109
+ opts.on('--[no-]html', 'Output the trace with ANSI escape code ' \
110
+ 'converted to HTML') do |value|
111
+ args.html = value
112
+ end
113
+
114
+ opts.on('-v', '--[no-]verbose', 'Show verbose output') do |value|
115
+ args.verbose = value
116
+ end
117
+
118
+ opts.on('--version', 'Print version information and exit') do
119
+ args.version = true
120
+ puts GitLabBuildOutput::VERSION
121
+ end
122
+
123
+ opts.on('-h', '--help', 'Show this message and exit') do
124
+ args.help = true
125
+ print_usage
126
+ end
127
+
128
+ opts.separator ''
129
+ opts.separator "Use the `-h' flag for help."
130
+
131
+ opts.parse!(raw_args)
132
+ opts
133
+ end
134
+ # rubocop:enable Metrics/AbcSize
135
+ # rubocop:enable Metrics/MethodLength
136
+
137
+ def handle_arguments(args)
138
+ if git_repository.nil?
139
+ print_usage
140
+ true
141
+ else
142
+ args.help || args.version
143
+ end
144
+ end
145
+
146
+ def banner
147
+ @banner ||= "Usage: gitlab-build-output [options] <git_repository>\n" \
148
+ "Version: #{GitLabBuildOutput::VERSION}"
149
+ end
150
+
151
+ def print_usage
152
+ puts option_parser.to_s
153
+ end
154
+ end
155
+ # rubocop:enable Metrics/ClassLength
156
+ end
@@ -0,0 +1,7 @@
1
+ class Hash
2
+ unless instance_methods(false).include?(:to_json)
3
+ def to_json
4
+ JSON.dump(self)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1 @@
1
+ require 'gitlab_build_output/core_ext/hash/to_json'
@@ -0,0 +1,143 @@
1
+ class Object
2
+ # An object is blank if it's false, empty, or a whitespace string.
3
+ # For example, +false+, '', ' ', +nil+, [], and {} are all blank.
4
+ #
5
+ # This simplifies
6
+ #
7
+ # !address || address.empty?
8
+ #
9
+ # to
10
+ #
11
+ # address.blank?
12
+ #
13
+ # @return [true, false]
14
+ def blank?
15
+ respond_to?(:empty?) ? !!empty? : !self
16
+ end
17
+
18
+ # An object is present if it's not blank.
19
+ #
20
+ # @return [true, false]
21
+ def present?
22
+ !blank?
23
+ end
24
+
25
+ # Returns the receiver if it's present otherwise returns +nil+.
26
+ # <tt>object.presence</tt> is equivalent to
27
+ #
28
+ # object.present? ? object : nil
29
+ #
30
+ # For example, something like
31
+ #
32
+ # state = params[:state] if params[:state].present?
33
+ # country = params[:country] if params[:country].present?
34
+ # region = state || country || 'US'
35
+ #
36
+ # becomes
37
+ #
38
+ # region = params[:state].presence || params[:country].presence || 'US'
39
+ #
40
+ # @return [Object]
41
+ def presence
42
+ self if present?
43
+ end
44
+ end
45
+
46
+ class NilClass
47
+ # +nil+ is blank:
48
+ #
49
+ # nil.blank? # => true
50
+ #
51
+ # @return [true]
52
+ def blank?
53
+ true
54
+ end
55
+ end
56
+
57
+ class FalseClass
58
+ # +false+ is blank:
59
+ #
60
+ # false.blank? # => true
61
+ #
62
+ # @return [true]
63
+ def blank?
64
+ true
65
+ end
66
+ end
67
+
68
+ class TrueClass
69
+ # +true+ is not blank:
70
+ #
71
+ # true.blank? # => false
72
+ #
73
+ # @return [false]
74
+ def blank?
75
+ false
76
+ end
77
+ end
78
+
79
+ class Array
80
+ # An array is blank if it's empty:
81
+ #
82
+ # [].blank? # => true
83
+ # [1,2,3].blank? # => false
84
+ #
85
+ # @return [true, false]
86
+ alias_method :blank?, :empty?
87
+ end
88
+
89
+ class Hash
90
+ # A hash is blank if it's empty:
91
+ #
92
+ # {}.blank? # => true
93
+ # { key: 'value' }.blank? # => false
94
+ #
95
+ # @return [true, false]
96
+ alias_method :blank?, :empty?
97
+ end
98
+
99
+ class String
100
+ BLANK_RE = /\A[[:space:]]*\z/
101
+
102
+ # A string is blank if it's empty or contains whitespaces only:
103
+ #
104
+ # ''.blank? # => true
105
+ # ' '.blank? # => true
106
+ # "\t\n\r".blank? # => true
107
+ # ' blah '.blank? # => false
108
+ #
109
+ # Unicode whitespace is supported:
110
+ #
111
+ # "\u00a0".blank? # => true
112
+ #
113
+ # @return [true, false]
114
+ def blank?
115
+ # The regexp that matches blank strings is expensive. For the case of empty
116
+ # strings we can speed up this method (~3.5x) with an empty? call. The
117
+ # penalty for the rest of strings is marginal.
118
+ empty? || BLANK_RE.match?(self)
119
+ end
120
+ end
121
+
122
+ class Numeric #:nodoc:
123
+ # No number is blank:
124
+ #
125
+ # 1.blank? # => false
126
+ # 0.blank? # => false
127
+ #
128
+ # @return [false]
129
+ def blank?
130
+ false
131
+ end
132
+ end
133
+
134
+ class Time #:nodoc:
135
+ # No Time is blank:
136
+ #
137
+ # Time.now.blank? # => false
138
+ #
139
+ # @return [false]
140
+ def blank?
141
+ false
142
+ end
143
+ end
@@ -0,0 +1 @@
1
+ require 'gitlab_build_output/core_ext/object/blank'
@@ -0,0 +1,9 @@
1
+ class Regexp #:nodoc:
2
+ def multiline?
3
+ options & MULTILINE == MULTILINE
4
+ end
5
+
6
+ def match?(string, pos = 0)
7
+ !!match(string, pos)
8
+ end unless //.respond_to?(:match?)
9
+ end
@@ -0,0 +1,3 @@
1
+ Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"].each do |path|
2
+ require path
3
+ end
@@ -0,0 +1,48 @@
1
+ module GitLabBuildOutput
2
+ class GitLabApi
3
+ class Status
4
+ CREATED = 'created'.freeze
5
+ PENDING = 'pending'.freeze
6
+ RUNNING = 'running'.freeze
7
+ FAILED = 'failed'.freeze
8
+ SUCCESS = 'success'.freeze
9
+ CANCELED = 'canceled'.freeze
10
+ SKIPPED = 'skipped'.freeze
11
+ MANUAL = 'manual'.freeze
12
+
13
+ def self.done?(status)
14
+ case status
15
+ when FAILED, SUCCESS, CANCELED, SKIPPED
16
+ true
17
+ else
18
+ false
19
+ end
20
+ end
21
+ end
22
+
23
+ def initialize(endpoint, private_token)
24
+ @client ||=
25
+ Gitlab.client(endpoint: endpoint, private_token: private_token)
26
+ end
27
+
28
+ def method_missing(method, *args, &block)
29
+ if @client.respond_to?(method)
30
+ @client.public_send(method, *args, &block)
31
+ else
32
+ super
33
+ end
34
+ end
35
+
36
+ def respond_to_missing?(*args)
37
+ @client.respond_to_missing(*args)
38
+ end
39
+
40
+ # def job_trace(project, job_id)
41
+ # options = {}
42
+ # @client.send(:set_authorization_header, options)
43
+ # url = @client.endpoint +
44
+ # "/projects/#{@client.url_encode(project)}/builds/#{id}/trace"
45
+ # @client.validate HTTParty.get(url, options)
46
+ # end
47
+ end
48
+ end
@@ -0,0 +1,11 @@
1
+ module GitLabBuildOutput
2
+ class HtmlOutputter
3
+ def output(trace)
4
+ puts ansi_to_html(trace) unless trace.empty?
5
+ end
6
+
7
+ def ansi_to_html(string)
8
+ Ansi2html.convert(StringIO.new(string)).html
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,72 @@
1
+ module GitLabBuildOutput
2
+ class JobTracer
3
+ def initialize(private_token, git_repository, endpoint = nil, https: false)
4
+ @https = https
5
+ @tracer = Tracer.new
6
+ @git = Git.open(git_repository)
7
+ self.endpoint = endpoint
8
+ @gitlab = GitLabApi.new(self.send(:endpoint), private_token)
9
+ end
10
+
11
+ def trace
12
+ last_job = send(:last_job)
13
+ trace = tracer.next_trace(job_trace(last_job.id))
14
+ [trace, last_job.status]
15
+ end
16
+
17
+ def last_job
18
+ gitlab
19
+ .commit_builds(project_name, last_commit, per_page: 10, page: 1)
20
+ .detect do |e|
21
+ e.status != GitLabApi::Status::CREATED &&
22
+ e.status != GitLabApi::Status::MANUAL
23
+ end
24
+ end
25
+
26
+ def last_commit
27
+ @last_commit ||= git.log[0].sha
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :git
33
+ attr_reader :gitlab
34
+ attr_reader :tracer
35
+ attr_reader :https
36
+ attr_reader :endpoint
37
+
38
+ def endpoint=(endpoint)
39
+ return @endpoint = endpoint if endpoint.present?
40
+ uri = URI.parse(parsed_url.host)
41
+ uri.path = '/api/v3'
42
+ uri.host = parsed_url.host
43
+ uri.scheme ||= parsed_url.scheme || scheme
44
+ uri.scheme = scheme if uri.scheme == 'ssh'
45
+
46
+ @endpoint = uri.to_s
47
+ end
48
+
49
+ def scheme
50
+ @scheme ||= https ? 'https' : 'http'
51
+ end
52
+
53
+ def project_name
54
+ @project_name ||= begin
55
+ path = parsed_url.path.gsub(/\A\/+/, '')
56
+ File.join(File.dirname(path), File.basename(path, File.extname(path)))
57
+ end
58
+ end
59
+
60
+ def git_url
61
+ @git_url ||= git.remote.url
62
+ end
63
+
64
+ def parsed_url
65
+ @parsed_url ||= GitCloneUrl.parse(git_url)
66
+ end
67
+
68
+ def job_trace(job_id)
69
+ gitlab.job_trace(project_name, job_id)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,11 @@
1
+ module GitLabBuildOutput
2
+ class LoopRunner < SingleRunner
3
+ def run
4
+ loop do
5
+ status = super
6
+ break if GitLabApi::Status.done?(status)
7
+ sleep interval
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ module GitLabBuildOutput
2
+ class Outputter
3
+ def output(trace)
4
+ puts trace unless trace.empty?
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,21 @@
1
+ module GitLabBuildOutput
2
+ class SingleRunner
3
+ def initialize(job_tracer, outputter, interval)
4
+ @job_tracer = job_tracer
5
+ @outputter = outputter
6
+ @interval = interval
7
+ end
8
+
9
+ def run
10
+ trace, status = job_tracer.trace
11
+ outputter.output(trace)
12
+ status
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :job_tracer
18
+ attr_reader :outputter
19
+ attr_reader :interval
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ module GitLabBuildOutput
2
+ class Tracer
3
+ def initialize
4
+ @previous_trace = ''
5
+ end
6
+
7
+ def next_trace(trace)
8
+ new_trace = diff(previous_trace, trace)
9
+ return '' if new_trace.nil?
10
+ previous_trace << new_trace
11
+ new_trace
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :previous_trace
17
+
18
+ def diff(s1, s2)
19
+ s1 ||= ''
20
+ s2 ||= ''
21
+
22
+ return '' if s1 == s2
23
+ index = s2.chars.zip(s1.chars).index { |a, b| a != b }
24
+ return nil if index.nil?
25
+ s2[index..-1]
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module GitLabBuildOutput
2
+ VERSION = '0.0.1'.freeze
3
+ end
@@ -0,0 +1,29 @@
1
+ require 'base64'
2
+ require 'erb'
3
+ require 'json'
4
+ require 'ostruct'
5
+ require 'stringio'
6
+ require 'strscan'
7
+
8
+ require 'git'
9
+ require 'gitlab'
10
+ require 'git_clone_url'
11
+ require 'httparty'
12
+
13
+ require 'gitlab_build_output/core_ext'
14
+
15
+ require 'gitlab_build_output/ansi2html'
16
+ require 'gitlab_build_output/application'
17
+ require 'gitlab_build_output/gitlab_api'
18
+ require 'gitlab_build_output/html_outputter'
19
+ require 'gitlab_build_output/job_tracer'
20
+ require 'gitlab_build_output/outputter'
21
+ require 'gitlab_build_output/tracer'
22
+ require 'gitlab_build_output/version'
23
+
24
+ # the order of these are important
25
+ require 'gitlab_build_output/single_runner'
26
+ require 'gitlab_build_output/loop_runner'
27
+
28
+ module GitLabBuildOutput
29
+ end
metadata ADDED
@@ -0,0 +1,216 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gitlab-build-output
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jacob Carlborg
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-01-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: git
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: gitlab
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: git_clone_url
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.14'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '1.14'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '3.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: '3.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '='
102
+ - !ruby/object:Gem::Version
103
+ version: 0.48.1
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '='
109
+ - !ruby/object:Gem::Version
110
+ version: 0.48.1
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: '0.11'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ~>
123
+ - !ruby/object:Gem::Version
124
+ version: '0.11'
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry-rescue
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ~>
130
+ - !ruby/object:Gem::Version
131
+ version: '1.4'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ~>
137
+ - !ruby/object:Gem::Version
138
+ version: '1.4'
139
+ - !ruby/object:Gem::Dependency
140
+ name: pry-stack_explorer
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ~>
144
+ - !ruby/object:Gem::Version
145
+ version: 0.4.9
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ~>
151
+ - !ruby/object:Gem::Version
152
+ version: 0.4.9
153
+ description: Prints the output of a GitLab CI job.
154
+ email:
155
+ - doob@me.com
156
+ executables:
157
+ - gitlab-build-output
158
+ extensions: []
159
+ extra_rdoc_files: []
160
+ files:
161
+ - .gitignore
162
+ - .rspec
163
+ - .rubocop.yml
164
+ - .ruby-gemset
165
+ - .rvm-gemset
166
+ - .travis.yml
167
+ - Gemfile
168
+ - README.md
169
+ - Rakefile
170
+ - bin/console
171
+ - bin/setup
172
+ - exe/gitlab-build-output
173
+ - gitlab-build-output.gemspec
174
+ - lib/gitlab/build/output.rb
175
+ - lib/gitlab_build_output.rb
176
+ - lib/gitlab_build_output/ansi2html.rb
177
+ - lib/gitlab_build_output/application.rb
178
+ - lib/gitlab_build_output/core_ext.rb
179
+ - lib/gitlab_build_output/core_ext/hash.rb
180
+ - lib/gitlab_build_output/core_ext/hash/to_json.rb
181
+ - lib/gitlab_build_output/core_ext/object.rb
182
+ - lib/gitlab_build_output/core_ext/object/blank.rb
183
+ - lib/gitlab_build_output/core_ext/regexp.rb
184
+ - lib/gitlab_build_output/gitlab_api.rb
185
+ - lib/gitlab_build_output/html_outputter.rb
186
+ - lib/gitlab_build_output/job_tracer.rb
187
+ - lib/gitlab_build_output/loop_runner.rb
188
+ - lib/gitlab_build_output/outputter.rb
189
+ - lib/gitlab_build_output/single_runner.rb
190
+ - lib/gitlab_build_output/tracer.rb
191
+ - lib/gitlab_build_output/version.rb
192
+ homepage: https://github.com/jacob-carlborg/gitlab-build-output
193
+ licenses: []
194
+ metadata:
195
+ allowed_push_host: https://rubygems.org
196
+ post_install_message:
197
+ rdoc_options: []
198
+ require_paths:
199
+ - lib
200
+ required_ruby_version: !ruby/object:Gem::Requirement
201
+ requirements:
202
+ - - '>='
203
+ - !ruby/object:Gem::Version
204
+ version: '0'
205
+ required_rubygems_version: !ruby/object:Gem::Requirement
206
+ requirements:
207
+ - - '>='
208
+ - !ruby/object:Gem::Version
209
+ version: '0'
210
+ requirements: []
211
+ rubyforge_project:
212
+ rubygems_version: 2.4.8
213
+ signing_key:
214
+ specification_version: 4
215
+ summary: Prints the output of a GitLab CI job.
216
+ test_files: []