gitlab-build-output 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []