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