circleci-cli 1.0.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,70 +1,73 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'circleci/cli/command/watch_command/build_repository'
4
+ require 'circleci/cli/command/watch_command/build_watcher'
5
+
3
6
  module CircleCI
4
7
  module CLI
5
8
  module Command
6
9
  class WatchCommand < BaseCommand
7
10
  class << self
8
- def run(options)
11
+ def run(options) # rubocop:disable Metrics/MethodLength
9
12
  setup_token
10
- setup_client
11
13
 
12
- build = get_build(options)
14
+ username, reponame = project_name(options).split('/')
15
+ @options = options
16
+ @repository = BuildRepository.new(
17
+ username,
18
+ reponame,
19
+ branch: branch_name(options),
20
+ user: options.user
21
+ )
22
+ @client = Networking::CircleCIPusherClient.new.tap(&:connect)
23
+ @build_watcher = nil
24
+
25
+ bind_status_event
13
26
 
14
- if build&.running?
15
- start_watch(build)
16
- wait_until_finish
17
- finalize(build, build.channel_name)
18
- else
19
- say 'The build is not running'
27
+ loop do
28
+ stop_existing_watcher_if_needed
29
+ start_watcher_if_needed
30
+ sleep 1
20
31
  end
32
+ rescue Interrupt
33
+ say 'Exited'
21
34
  end
22
35
 
23
36
  private
24
37
 
25
- def setup_client
26
- @client = Networking::CircleCIPusherClient.new
27
- @client.connect
38
+ def bind_status_event
39
+ @client.bind("private-#{Response::Account.me.pusher_id}", 'call') { @repository.update }
28
40
  end
29
41
 
30
- def get_build(options)
31
- username, reponame = project_name(options).split('/')
32
- number = build_number options
33
- Response::Build.get(username, reponame, number)
34
- end
42
+ def stop_existing_watcher_if_needed
43
+ return if @build_watcher.nil?
35
44
 
36
- def start_watch(build)
37
- @running = true
38
- text = "Start watching #{build.project_name} ##{build.build_number}"
39
- print_bordered text
40
- TerminalNotifier.notify text
45
+ build = @repository.build_for(@build_watcher.build.build_number)
46
+ return if build.nil? || !build.finished?
41
47
 
42
- bind_event_handling build.channel_name
48
+ @build_watcher.stop(build.status)
49
+ @build_watcher = nil
50
+ show_interrupted_build_results
43
51
  end
44
52
 
45
- def bind_event_handling(channel)
46
- @client.bind_event_json(channel, 'newAction') do |json|
47
- print_bordered json['log']['name'].green
48
- end
49
-
50
- @client.bind_event_json(channel, 'appendAction') do |json|
51
- say json['out']['message']
52
- end
53
-
54
- @client.bind_event_json(channel, 'updateAction') do |json|
55
- @running = json['log']['name'] != 'Disable SSH'
56
- end
57
- end
53
+ def start_watcher_if_needed
54
+ build_to_watch = @repository.builds_to_show.select(&:running?).first
55
+ return unless build_to_watch && @build_watcher.nil?
58
56
 
59
- def wait_until_finish
60
- sleep(1) while @running
57
+ show_interrupted_build_results
58
+ @repository.mark_as_shown(build_to_watch.build_number)
59
+ @build_watcher = BuildWatcher.new(build_to_watch, verbose: @options.verbose)
60
+ @build_watcher.start
61
61
  end
62
62
 
63
- def finalize(build, channel)
64
- @client.unsubscribe(channel)
65
- text = "Finish watching #{build.project_name} ##{build.build_number}"
66
- print_bordered text.blue
67
- TerminalNotifier.notify text
63
+ def show_interrupted_build_results # rubocop:disable Metrics/AbcSize
64
+ @repository.builds_to_show.select(&:finished?).each do |build|
65
+ b = Response::Build.get(build.username, build.reponame, build.build_number)
66
+ title = "✅ Result of #{build.project_name} ##{build.build_number} completed in background"
67
+ say Printer::BuildPrinter.header_for(build, title)
68
+ say Printer::StepPrinter.new(b.steps, pretty: @options.verbose).to_s
69
+ @repository.mark_as_shown(b.build_number)
70
+ end
68
71
  end
69
72
 
70
73
  def print_bordered(text)
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CircleCI
4
+ module CLI
5
+ module Command
6
+ class BuildRepository
7
+ def initialize(username, reponame, branch: nil, user: nil)
8
+ @username = username
9
+ @user = user
10
+ @reponame = reponame
11
+ @branch = branch
12
+ @builds = Response::Build.all(@username, @reponame)
13
+ @build_numbers_shown = @builds.select(&:finished?).map(&:build_number)
14
+ end
15
+
16
+ def update
17
+ response = if @branch
18
+ Response::Build.branch(@username, @reponame, @branch)
19
+ else
20
+ Response::Build.all(@username, @reponame)
21
+ end
22
+
23
+ @builds = (response + @builds).uniq(&:build_number)
24
+ end
25
+
26
+ def mark_as_shown(build_number)
27
+ @build_numbers_shown = (@build_numbers_shown + [build_number]).uniq
28
+ end
29
+
30
+ def builds_to_show
31
+ @builds
32
+ .reject { |build| @build_numbers_shown.include?(build.build_number) }
33
+ .select { |build| @branch.nil? || build.branch.to_s == @branch.to_s }
34
+ .select { |build| @user.nil? || build.user.to_s == @user.to_s }
35
+ .sort_by(&:build_number)
36
+ end
37
+
38
+ def build_for(build_number)
39
+ @builds.find { |build| build.build_number == build_number }
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CircleCI
4
+ module CLI
5
+ module Command
6
+ class BuildWatcher
7
+ attr_reader :build
8
+
9
+ def initialize(build, verbose: false)
10
+ @build = build
11
+ @verbose = verbose
12
+ @messages = Hash.new { |h, k| h[k] = [] }
13
+ end
14
+
15
+ def start
16
+ bind_event_handling @build.channel_name
17
+ notify_started
18
+ end
19
+
20
+ def stop(status)
21
+ client.unsubscribe("#{@build.channel_name}@0")
22
+ notify_stopped(status)
23
+ end
24
+
25
+ private
26
+
27
+ def bind_event_handling(channel) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
28
+ client.bind_event_json(channel, 'newAction') do |json|
29
+ if @verbose
30
+ print_bordered json['log']['name']
31
+ else
32
+ print json['log']['name']
33
+ end
34
+ end
35
+
36
+ client.bind_event_json(channel, 'appendAction') do |json|
37
+ if @verbose
38
+ Thor::Shell::Basic.new.say(json['out']['message'], nil, false)
39
+ else
40
+ @messages[json['step']] << json['out']['message']
41
+ end
42
+ end
43
+
44
+ client.bind_event_json(channel, 'updateAction') do |json|
45
+ next if @verbose
46
+
47
+ case json['log']['status']
48
+ when 'success'
49
+ puts "\e[2K\r#{Printer.colorize_green(json['log']['name'])}"
50
+ when 'failed'
51
+ puts "\e[2K\r#{Printer.colorize_red(json['log']['name'])}"
52
+ @messages[json['step']].each(&method(:say))
53
+ end
54
+ end
55
+ end
56
+
57
+ def notify_started
58
+ say Printer::BuildPrinter.header_for(
59
+ @build,
60
+ "👀 Start watching #{@build.project_name} ##{@build.build_number}"
61
+ )
62
+ end
63
+
64
+ def notify_stopped(status)
65
+ text = case status
66
+ when 'success'
67
+ Printer.colorize_green("🎉 #{@build.project_name} ##{@build.build_number} has succeeded!")
68
+ when 'failed'
69
+ Printer.colorize_red("😥 #{@build.project_name} ##{@build.build_number} has failed...")
70
+ end
71
+
72
+ @verbose ? print_bordered(text) : say(text)
73
+ end
74
+
75
+ def print_bordered(text)
76
+ say Terminal::Table.new(rows: [[text]], style: { width: 120 }).to_s
77
+ end
78
+
79
+ def client
80
+ @client ||= Networking::CircleCIPusherClient.new.tap(&:connect)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -8,45 +8,39 @@ module CircleCI
8
8
  class CircleCIPusherClient
9
9
  def connect
10
10
  PusherClient.logger.level = Logger::ERROR
11
- @socket = PusherClient::Socket.new(app_key, pusher_options)
12
- @socket.connect(true)
11
+ socket.connect(true)
13
12
  end
14
13
 
15
- def bind(channel, event)
16
- @socket.subscribe(channel)
17
- @socket[channel].bind(event) do |data|
18
- yield data
19
- end
14
+ def bind(channel, event, &block)
15
+ socket.subscribe(channel)
16
+ socket[channel].bind(event, &block)
20
17
  end
21
18
 
22
- def bind_event_json(channel, event)
23
- bind(channel, event) do |data|
24
- JSON.parse(data).each { |json| yield(json) }
25
- end
19
+ def bind_event_json(channel, event, &block)
20
+ bind(channel, event) { |data| JSON.parse(data).each(&block) }
26
21
  end
27
22
 
28
23
  def unsubscribe(channel)
29
- @socket.unsubscribe(channel)
24
+ socket.unsubscribe(channel)
30
25
  end
31
26
 
32
27
  private
33
28
 
34
- def app_key
35
- '1cf6e0e755e419d2ac9a'
36
- end
37
-
38
- def pusher_options
39
- {
29
+ def socket
30
+ @socket ||= PusherClient::Socket.new(
31
+ '1cf6e0e755e419d2ac9a',
40
32
  secure: true,
41
33
  auth_method: proc { |a, b| auth(a, b) },
42
34
  logger: Logger.new('/dev/null')
43
- }
35
+ )
44
36
  end
45
37
 
46
38
  def auth(socket_id, channel)
47
- data = { socket_id: socket_id, channel_name: channel.name }
48
39
  token = ENV['CIRCLE_CI_TOKEN'] || ask('Circle CI token ? :')
49
- res = connection.post("/auth/pusher?circle-token=#{token}", data)
40
+ res = connection.post(
41
+ "/auth/pusher?circle-token=#{token}",
42
+ { socket_id: socket_id, channel_name: channel.name }
43
+ )
50
44
  JSON.parse(res.body)['auth']
51
45
  end
52
46
 
@@ -6,6 +6,30 @@ require 'circleci/cli/printer/step_printer'
6
6
 
7
7
  module CircleCI
8
8
  module CLI
9
- module Printer; end
9
+ module Printer
10
+ class << self
11
+ def colorize_red(string)
12
+ colorize(string, '0;31;49')
13
+ end
14
+
15
+ def colorize_green(string)
16
+ colorize(string, '0;32;49')
17
+ end
18
+
19
+ def colorize_yellow(string)
20
+ colorize(string, '0;33;49')
21
+ end
22
+
23
+ def colorize_light_black(string)
24
+ colorize(string, '0;90;49')
25
+ end
26
+
27
+ private
28
+
29
+ def colorize(string, color_code)
30
+ "\e[#{color_code}m#{string}\e[0m"
31
+ end
32
+ end
33
+ end
10
34
  end
11
35
  end
@@ -4,8 +4,21 @@ module CircleCI
4
4
  module CLI
5
5
  module Printer
6
6
  class BuildPrinter
7
- def initialize(builds, pretty: true)
8
- @builds = builds
7
+ class << self
8
+ def header_for(build, title)
9
+ texts = [
10
+ ["Project: #{build.project_name}"],
11
+ ["Build: #{build.build_number}"],
12
+ ["Author: #{build.author_name}"],
13
+ ["Workflow: #{build.workflow_name}/#{build.workflow_job_name}"]
14
+ ]
15
+ Terminal::Table.new(title: title, rows: texts, style: { width: 120 }).to_s
16
+ end
17
+ end
18
+
19
+ def initialize(builds, project_name, pretty: true)
20
+ @builds_to_show = builds
21
+ @project_name = project_name
9
22
  @pretty = pretty
10
23
  end
11
24
 
@@ -24,8 +37,7 @@ module CircleCI
24
37
  end
25
38
 
26
39
  def title
27
- build = @builds.first
28
- "Recent Builds / #{build.project_name}".green
40
+ Printer.colorize_green("Recent Builds / #{@project_name}")
29
41
  end
30
42
 
31
43
  def headings
@@ -33,11 +45,11 @@ module CircleCI
33
45
  end
34
46
 
35
47
  def rows
36
- @builds.map(&:information)
48
+ @builds_to_show.map(&:information)
37
49
  end
38
50
 
39
51
  def max_row_widths
40
- @builds
52
+ @builds_to_show
41
53
  .map(&:information)
42
54
  .map { |array| array.map(&:to_s).map(&:size) }
43
55
  .transpose
@@ -5,6 +5,7 @@ module CircleCI
5
5
  module Printer
6
6
  class ProjectPrinter
7
7
  attr_accessor :compact
8
+
8
9
  def initialize(projects, pretty: true)
9
10
  @projects = projects
10
11
  @pretty = pretty
@@ -26,7 +27,7 @@ module CircleCI
26
27
 
27
28
  def print_pretty
28
29
  Terminal::Table.new(
29
- title: 'Projects'.green,
30
+ title: Printer.colorize_green('Projects'),
30
31
  headings: ['User name', 'Repository name'],
31
32
  rows: @projects.map(&:information)
32
33
  ).to_s
@@ -9,26 +9,36 @@ module CircleCI
9
9
  @pretty = pretty
10
10
  end
11
11
 
12
- def to_s
13
- Terminal::Table.new do |t|
14
- @steps
15
- .group_by(&:type)
16
- .each do |key, steps|
17
- t << :separator
18
- t << [{ value: key.green, alignment: :center, colspan: 2 }]
19
- steps.each { |s| print_actions(t, s) }
20
- end
21
- end.to_s
12
+ def to_s # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
13
+ if @pretty
14
+ Terminal::Table.new do |t|
15
+ @steps
16
+ .group_by(&:type)
17
+ .each do |key, steps|
18
+ t << :separator
19
+ t << [{ value: Printer.colorize_green(key), alignment: :center, colspan: 2 }]
20
+ steps.each { |s| print_actions(t, s) }
21
+ end
22
+ end.to_s
23
+ else
24
+ @steps.group_by(&:type).map do |_, steps|
25
+ steps.map do |step|
26
+ step.actions.map do |a|
27
+ "#{colorize_by_status(a.name.slice(0..120), a.status)}\n#{"#{a.log}\n" if a.failed? && a.log}"
28
+ end
29
+ end.flatten.join('')
30
+ end.join("\n")
31
+ end
22
32
  end
23
33
 
24
34
  private
25
35
 
26
36
  def colorize_by_status(string, status)
27
37
  case status
28
- when 'success', 'fixed' then string.green
29
- when 'canceled' then string.yellow
30
- when 'failed', 'timedout' then string.red
31
- when 'no_tests', 'not_run' then string.light_black
38
+ when 'success', 'fixed' then Printer.colorize_green(string)
39
+ when 'canceled' then Printer.colorize_yellow(string)
40
+ when 'failed', 'timedout' then Printer.colorize_red(string)
41
+ when 'no_tests', 'not_run' then Printer.colorize_light_black(string)
32
42
  else string
33
43
  end
34
44
  end
@@ -36,8 +46,8 @@ module CircleCI
36
46
  def format_time(time)
37
47
  return '' unless time
38
48
 
39
- minute = format('%02d', time / 1000 / 60)
40
- second = format('%02d', (time / 1000) % 60)
49
+ minute = format('%<time>02d', time: time / 1000 / 60)
50
+ second = format('%<time>02d', time: (time / 1000) % 60)
41
51
  "#{minute}:#{second}"
42
52
  end
43
53