circleci-cli 1.0.0 → 3.0.0

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.
@@ -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