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.
- checksums.yaml +4 -4
- data/.all-contributorsrc +56 -0
- data/.circleci/config.yml +2 -2
- data/.github/workflows/test.yml +22 -0
- data/.rubocop.yml +9 -1
- data/.ruby-version +1 -0
- data/CHANGELOG.md +140 -0
- data/Gemfile.lock +93 -62
- data/README.md +74 -14
- data/Rakefile +8 -0
- data/circleci-cli.gemspec +6 -5
- data/lib/circleci/cli.rb +115 -24
- data/lib/circleci/cli/command/base_command.rb +6 -13
- data/lib/circleci/cli/command/build_command.rb +18 -2
- data/lib/circleci/cli/command/builds_command.rb +9 -9
- data/lib/circleci/cli/command/projects_command.rb +1 -1
- data/lib/circleci/cli/command/retry_command.rb +18 -2
- data/lib/circleci/cli/command/watch_command.rb +46 -43
- data/lib/circleci/cli/command/watch_command/build_repository.rb +44 -0
- data/lib/circleci/cli/command/watch_command/build_watcher.rb +85 -0
- data/lib/circleci/cli/networking/pusher_client.rb +15 -21
- data/lib/circleci/cli/printer.rb +25 -1
- data/lib/circleci/cli/printer/build_printer.rb +18 -6
- data/lib/circleci/cli/printer/project_printer.rb +2 -1
- data/lib/circleci/cli/printer/step_printer.rb +26 -16
- data/lib/circleci/cli/response/account.rb +6 -4
- data/lib/circleci/cli/response/action.rb +1 -0
- data/lib/circleci/cli/response/build.rb +35 -28
- data/lib/circleci/cli/response/step.rb +1 -2
- data/lib/circleci/cli/version.rb +1 -1
- data/movie/rec.gif +0 -0
- metadata +45 -27
@@ -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
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
26
|
-
@client
|
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
|
31
|
-
|
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
|
-
|
37
|
-
|
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
|
-
|
48
|
+
@build_watcher.stop(build.status)
|
49
|
+
@build_watcher = nil
|
50
|
+
show_interrupted_build_results
|
43
51
|
end
|
44
52
|
|
45
|
-
def
|
46
|
-
@
|
47
|
-
|
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
|
-
|
60
|
-
|
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
|
64
|
-
@
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
12
|
-
@socket.connect(true)
|
11
|
+
socket.connect(true)
|
13
12
|
end
|
14
13
|
|
15
|
-
def bind(channel, event)
|
16
|
-
|
17
|
-
|
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)
|
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
|
-
|
24
|
+
socket.unsubscribe(channel)
|
30
25
|
end
|
31
26
|
|
32
27
|
private
|
33
28
|
|
34
|
-
def
|
35
|
-
|
36
|
-
|
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(
|
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
|
|
data/lib/circleci/cli/printer.rb
CHANGED
@@ -6,6 +6,30 @@ require 'circleci/cli/printer/step_printer'
|
|
6
6
|
|
7
7
|
module CircleCI
|
8
8
|
module CLI
|
9
|
-
module Printer
|
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
|
-
|
8
|
-
|
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
|
-
|
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
|
-
@
|
48
|
+
@builds_to_show.map(&:information)
|
37
49
|
end
|
38
50
|
|
39
51
|
def max_row_widths
|
40
|
-
@
|
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'
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
29
|
-
when 'canceled' then string
|
30
|
-
when 'failed', 'timedout' then string
|
31
|
-
when 'no_tests', 'not_run' then string
|
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('
|
40
|
-
second = format('
|
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
|
|