fun_ci 1.1.0 → 1.2.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/CHANGELOG.md +19 -0
- data/exe/fun-ci-trigger +6 -6
- data/exe/fun-ci-tui +6 -6
- data/lib/fun_ci/cli.rb +17 -16
- data/lib/fun_ci/persistence/database.rb +52 -0
- data/lib/fun_ci/persistence/pipeline_recorder.rb +74 -0
- data/lib/fun_ci/persistence/pipeline_run.rb +59 -0
- data/lib/fun_ci/persistence/stage_job.rb +46 -0
- data/lib/fun_ci/{state_machine.rb → persistence/state_machine.rb} +3 -12
- data/lib/fun_ci/pipeline/background_wrapper.rb +27 -0
- data/lib/fun_ci/pipeline/pipeline_forker.rb +38 -0
- data/lib/fun_ci/pipeline/process_runner.rb +28 -0
- data/lib/fun_ci/pipeline/progress_reporter.rb +31 -0
- data/lib/fun_ci/pipeline/stage_runner.rb +85 -0
- data/lib/fun_ci/pipeline/stale_pipeline_canceller.rb +53 -0
- data/lib/fun_ci/pipeline/trigger.rb +153 -0
- data/lib/fun_ci/setup/hook_writer.rb +75 -0
- data/lib/fun_ci/setup/installer.rb +55 -0
- data/lib/fun_ci/setup/maven_linter_detector.rb +26 -0
- data/lib/fun_ci/setup/project_config.rb +42 -0
- data/lib/fun_ci/setup/project_detector.rb +22 -0
- data/lib/fun_ci/setup/setup_checker.rb +30 -0
- data/lib/fun_ci/setup/template_writer.rb +53 -0
- data/lib/fun_ci/tui/admin_tui.rb +90 -0
- data/lib/fun_ci/tui/animation.rb +49 -0
- data/lib/fun_ci/tui/animation_compositor.rb +107 -0
- data/lib/fun_ci/tui/animation_frames.rb +112 -0
- data/lib/fun_ci/tui/animation_library.rb +46 -0
- data/lib/fun_ci/tui/animation_renderer.rb +144 -0
- data/lib/fun_ci/tui/ansi.rb +34 -0
- data/lib/fun_ci/tui/board_data.rb +53 -0
- data/lib/fun_ci/tui/board_renderer.rb +105 -0
- data/lib/fun_ci/tui/duration_formatter.rb +24 -0
- data/lib/fun_ci/tui/header_animation_manager.rb +71 -0
- data/lib/fun_ci/tui/header_animation_player.rb +45 -0
- data/lib/fun_ci/tui/key_handler.rb +86 -0
- data/lib/fun_ci/tui/looping_animation_player.rb +45 -0
- data/lib/fun_ci/tui/relative_time.rb +22 -0
- data/lib/fun_ci/tui/row_formatter.rb +108 -0
- data/lib/fun_ci/tui/screen.rb +103 -0
- data/lib/fun_ci/tui/spinner.rb +24 -0
- data/lib/fun_ci/tui/stage_change_detector.rb +58 -0
- data/lib/fun_ci/tui/streak_counter.rb +29 -0
- data/lib/fun_ci/tui/terminal_input.rb +69 -0
- data/lib/fun_ci.rb +6 -6
- metadata +41 -37
- data/lib/fun_ci/admin_tui.rb +0 -238
- data/lib/fun_ci/animation.rb +0 -47
- data/lib/fun_ci/animation_compositor.rb +0 -105
- data/lib/fun_ci/animation_frames.rb +0 -111
- data/lib/fun_ci/animation_library.rb +0 -44
- data/lib/fun_ci/animation_renderer.rb +0 -142
- data/lib/fun_ci/ansi.rb +0 -32
- data/lib/fun_ci/background_wrapper.rb +0 -27
- data/lib/fun_ci/board_data.rb +0 -51
- data/lib/fun_ci/database.rb +0 -50
- data/lib/fun_ci/duration_formatter.rb +0 -23
- data/lib/fun_ci/header_animation_manager.rb +0 -69
- data/lib/fun_ci/header_animation_player.rb +0 -43
- data/lib/fun_ci/hook_writer.rb +0 -73
- data/lib/fun_ci/installer.rb +0 -53
- data/lib/fun_ci/looping_animation_player.rb +0 -43
- data/lib/fun_ci/maven_linter_detector.rb +0 -24
- data/lib/fun_ci/pipeline_forker.rb +0 -36
- data/lib/fun_ci/pipeline_recorder.rb +0 -72
- data/lib/fun_ci/pipeline_run.rb +0 -57
- data/lib/fun_ci/progress_reporter.rb +0 -29
- data/lib/fun_ci/project_config.rb +0 -40
- data/lib/fun_ci/project_detector.rb +0 -18
- data/lib/fun_ci/relative_time.rb +0 -20
- data/lib/fun_ci/row_formatter.rb +0 -106
- data/lib/fun_ci/screen.rb +0 -95
- data/lib/fun_ci/setup_checker.rb +0 -28
- data/lib/fun_ci/spinner.rb +0 -22
- data/lib/fun_ci/stage_change_detector.rb +0 -56
- data/lib/fun_ci/stage_job.rb +0 -44
- data/lib/fun_ci/stage_runner.rb +0 -108
- data/lib/fun_ci/stale_pipeline_canceller.rb +0 -51
- data/lib/fun_ci/streak_counter.rb +0 -30
- data/lib/fun_ci/template_writer.rb +0 -51
- data/lib/fun_ci/trigger.rb +0 -150
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ansi"
|
|
4
|
+
|
|
5
|
+
module FunCi
|
|
6
|
+
module Tui
|
|
7
|
+
class HeaderAnimationPlayer
|
|
8
|
+
attr_reader :frame
|
|
9
|
+
|
|
10
|
+
def initialize(data)
|
|
11
|
+
@data = data
|
|
12
|
+
@frame = 0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def advance!
|
|
16
|
+
@frame += 1
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def finished?
|
|
20
|
+
@frame >= total_frames
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def total_frames
|
|
24
|
+
@data[:frames].length
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def current_lines(width)
|
|
28
|
+
return [] if finished?
|
|
29
|
+
|
|
30
|
+
frame_lines = @data[:frames][@frame]
|
|
31
|
+
return [] unless frame_lines
|
|
32
|
+
|
|
33
|
+
frame_lines.map { |line| center_line(line, width) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def center_line(line, width)
|
|
39
|
+
visible_len = Ansi.strip(line).length
|
|
40
|
+
pad = [(width - visible_len) / 2, 0].max
|
|
41
|
+
"#{" " * pad}#{line}\e[K"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Tui
|
|
5
|
+
class KeyHandler
|
|
6
|
+
attr_reader :cursor_index
|
|
7
|
+
|
|
8
|
+
def initialize(board_data:)
|
|
9
|
+
@board_data = board_data
|
|
10
|
+
@cursor_index = nil
|
|
11
|
+
@confirm_cancel = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def handle_key(key)
|
|
15
|
+
if @confirm_cancel
|
|
16
|
+
handle_confirm_key(key)
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
case key
|
|
21
|
+
when "q"
|
|
22
|
+
:quit
|
|
23
|
+
when "j", :down
|
|
24
|
+
move_cursor_down
|
|
25
|
+
when "k", :up
|
|
26
|
+
move_cursor_up
|
|
27
|
+
when "c"
|
|
28
|
+
initiate_cancel
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def confirming?
|
|
33
|
+
!@confirm_cancel.nil?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def confirmation_run
|
|
37
|
+
@confirm_cancel
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def move_cursor_down
|
|
43
|
+
runs = @board_data.runs
|
|
44
|
+
return if runs.empty?
|
|
45
|
+
|
|
46
|
+
if @cursor_index.nil?
|
|
47
|
+
@cursor_index = 0
|
|
48
|
+
elsif @cursor_index < runs.length - 1
|
|
49
|
+
@cursor_index += 1
|
|
50
|
+
@board_data.load_more if @cursor_index == runs.length - 1
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def move_cursor_up
|
|
55
|
+
return if @cursor_index.nil?
|
|
56
|
+
|
|
57
|
+
@cursor_index -= 1 if @cursor_index > 0
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def initiate_cancel
|
|
61
|
+
return if @cursor_index.nil?
|
|
62
|
+
|
|
63
|
+
runs = @board_data.runs
|
|
64
|
+
return if @cursor_index >= runs.length
|
|
65
|
+
|
|
66
|
+
run = runs[@cursor_index]
|
|
67
|
+
case run[:status]
|
|
68
|
+
when "scheduled"
|
|
69
|
+
@board_data.cancel_run(run[:id])
|
|
70
|
+
when "running"
|
|
71
|
+
@confirm_cancel = run
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def handle_confirm_key(key)
|
|
76
|
+
case key
|
|
77
|
+
when "y"
|
|
78
|
+
@board_data.cancel_run(@confirm_cancel[:id])
|
|
79
|
+
@confirm_cancel = nil
|
|
80
|
+
when "n", :escape
|
|
81
|
+
@confirm_cancel = nil
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ansi"
|
|
4
|
+
|
|
5
|
+
module FunCi
|
|
6
|
+
module Tui
|
|
7
|
+
class LoopingAnimationPlayer
|
|
8
|
+
def initialize(data)
|
|
9
|
+
@data = data
|
|
10
|
+
@frame = 0
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def advance!
|
|
14
|
+
@frame += 1
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def frame
|
|
18
|
+
@frame % total_frames
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def finished?
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def total_frames
|
|
26
|
+
@data[:frames].length
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def current_lines(width)
|
|
30
|
+
frame_lines = @data[:frames][frame]
|
|
31
|
+
return [] unless frame_lines
|
|
32
|
+
|
|
33
|
+
frame_lines.map { |line| center_line(line, width) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def center_line(line, width)
|
|
39
|
+
visible_len = Ansi.strip(line).length
|
|
40
|
+
pad = [(width - visible_len) / 2, 0].max
|
|
41
|
+
"#{" " * pad}#{line}\e[K"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module FunCi
|
|
6
|
+
module Tui
|
|
7
|
+
module RelativeTime
|
|
8
|
+
def self.format(iso_timestamp, now: Time.now)
|
|
9
|
+
elapsed = now - Time.parse(iso_timestamp)
|
|
10
|
+
seconds = elapsed.to_i
|
|
11
|
+
|
|
12
|
+
if seconds < 60
|
|
13
|
+
"just now"
|
|
14
|
+
elsif seconds < 3600
|
|
15
|
+
"#{seconds / 60}m ago"
|
|
16
|
+
else
|
|
17
|
+
"#{seconds / 3600}h ago"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ansi"
|
|
4
|
+
require_relative "duration_formatter"
|
|
5
|
+
require_relative "relative_time"
|
|
6
|
+
|
|
7
|
+
module FunCi
|
|
8
|
+
module Tui
|
|
9
|
+
module RowFormatter
|
|
10
|
+
STAGE_NAMES = { "lint" => "Lint", "build" => "Build", "fast" => "Fast", "slow" => "Slow" }.freeze
|
|
11
|
+
STATUS_LABELS = {
|
|
12
|
+
"completed" => "PASSED", "failed" => "FAILED", "timed_out" => "TIMED OUT",
|
|
13
|
+
"running" => "RUNNING", "scheduled" => "Scheduled...", "cancelled" => "CANCELLED"
|
|
14
|
+
}.freeze
|
|
15
|
+
PROJECT_COLORS = %w[31 32 33 34 35 36 91 92 93 94].freeze
|
|
16
|
+
|
|
17
|
+
def self.format(run, now: Time.now, spinner_frame: nil, elapsed_seconds: nil)
|
|
18
|
+
commit = run[:commit_hash][0, 7]
|
|
19
|
+
branch = run[:branch]
|
|
20
|
+
status = run[:status]
|
|
21
|
+
project = format_project(run[:project_path])
|
|
22
|
+
|
|
23
|
+
case status
|
|
24
|
+
when "scheduled"
|
|
25
|
+
format_scheduled(commit, branch, project, run[:updated_at], now)
|
|
26
|
+
when "cancelled"
|
|
27
|
+
format_cancelled(commit, branch, project, run[:stages], run[:updated_at], now)
|
|
28
|
+
else
|
|
29
|
+
stages_text = format_stages(run[:stages], status, spinner_frame, elapsed_seconds)
|
|
30
|
+
status_text = format_status(status)
|
|
31
|
+
time_text = Ansi.dim(RelativeTime.format(run[:updated_at], now: now))
|
|
32
|
+
" #{commit} #{branch}#{project} #{stages_text} #{status_text} #{time_text}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.format_project(project_path)
|
|
37
|
+
return "" unless project_path
|
|
38
|
+
name = File.basename(project_path)
|
|
39
|
+
code = PROJECT_COLORS[name.hash.abs % PROJECT_COLORS.size]
|
|
40
|
+
" \e[#{code}m#{name}#{Ansi::RESET}"
|
|
41
|
+
end
|
|
42
|
+
private_class_method :format_project
|
|
43
|
+
|
|
44
|
+
def self.format_scheduled(commit, branch, project, updated_at, now)
|
|
45
|
+
time_text = RelativeTime.format(updated_at, now: now)
|
|
46
|
+
Ansi.dim(" #{commit} #{branch}#{project} Scheduled... #{time_text}")
|
|
47
|
+
end
|
|
48
|
+
private_class_method :format_scheduled
|
|
49
|
+
|
|
50
|
+
def self.format_cancelled(commit, branch, project, stages, updated_at, now)
|
|
51
|
+
stages_text = stages.map { |s| format_cancelled_stage(s) }.join(" ")
|
|
52
|
+
time_text = RelativeTime.format(updated_at, now: now)
|
|
53
|
+
Ansi.dim(" #{commit} #{branch}#{project} #{stages_text} CANCELLED #{time_text}")
|
|
54
|
+
end
|
|
55
|
+
private_class_method :format_cancelled
|
|
56
|
+
|
|
57
|
+
def self.format_cancelled_stage(stage)
|
|
58
|
+
name = STAGE_NAMES[stage[:stage]]
|
|
59
|
+
if stage[:duration]
|
|
60
|
+
"#{name} #{DurationFormatter.format(stage[:duration])}"
|
|
61
|
+
else
|
|
62
|
+
"#{name} --"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
private_class_method :format_cancelled_stage
|
|
66
|
+
|
|
67
|
+
def self.format_stages(stages, run_status, spinner_frame, elapsed_seconds)
|
|
68
|
+
stages.map { |s| format_stage(s, spinner_frame, elapsed_seconds) }.join(" ")
|
|
69
|
+
end
|
|
70
|
+
private_class_method :format_stages
|
|
71
|
+
|
|
72
|
+
def self.format_stage(stage, spinner_frame, elapsed_seconds)
|
|
73
|
+
name = STAGE_NAMES[stage[:stage]]
|
|
74
|
+
|
|
75
|
+
case stage[:status]
|
|
76
|
+
when "completed"
|
|
77
|
+
time = DurationFormatter.format(stage[:duration])
|
|
78
|
+
Ansi.green("#{name} #{time}")
|
|
79
|
+
when "failed"
|
|
80
|
+
time = DurationFormatter.format(stage[:duration])
|
|
81
|
+
Ansi.bold_red("#{name} FAIL #{time}")
|
|
82
|
+
when "timed_out"
|
|
83
|
+
time = DurationFormatter.format(stage[:duration])
|
|
84
|
+
Ansi.bold_yellow("#{name} TIMEOUT #{time}")
|
|
85
|
+
when "running"
|
|
86
|
+
frame = spinner_frame || "\u2800"
|
|
87
|
+
time = elapsed_seconds ? "#{elapsed_seconds.to_i}s" : "--"
|
|
88
|
+
Ansi.cyan("#{name} #{frame} #{time}")
|
|
89
|
+
else
|
|
90
|
+
Ansi.dim("#{name} --")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
private_class_method :format_stage
|
|
94
|
+
|
|
95
|
+
def self.format_status(status)
|
|
96
|
+
label = STATUS_LABELS[status]
|
|
97
|
+
case status
|
|
98
|
+
when "completed" then Ansi.bold_green(label)
|
|
99
|
+
when "failed" then Ansi.bold_red(label)
|
|
100
|
+
when "timed_out" then Ansi.bold_yellow(label)
|
|
101
|
+
when "running" then Ansi.bold_cyan(label)
|
|
102
|
+
else Ansi.dim(label)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
private_class_method :format_status
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ansi"
|
|
4
|
+
|
|
5
|
+
module FunCi
|
|
6
|
+
module Tui
|
|
7
|
+
class Screen
|
|
8
|
+
attr_reader :width, :height
|
|
9
|
+
|
|
10
|
+
def initialize(output: $stdout, width: 80)
|
|
11
|
+
@output = output
|
|
12
|
+
@width = width
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def width=(new_width)
|
|
16
|
+
return if new_width == @width
|
|
17
|
+
|
|
18
|
+
clear
|
|
19
|
+
@width = new_width
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def height=(new_height)
|
|
23
|
+
return if new_height == @height
|
|
24
|
+
|
|
25
|
+
clear
|
|
26
|
+
@height = new_height
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def render_header(streak_text:)
|
|
30
|
+
title = " fun-ci"
|
|
31
|
+
if streak_text
|
|
32
|
+
streak_display = Ansi.green(streak_text)
|
|
33
|
+
plain_streak = streak_text
|
|
34
|
+
padding = @width - title.length - plain_streak.length
|
|
35
|
+
padding = 1 if padding < 1
|
|
36
|
+
line = "#{title}#{" " * padding}#{streak_display}"
|
|
37
|
+
else
|
|
38
|
+
line = title.ljust(@width)
|
|
39
|
+
end
|
|
40
|
+
println Ansi.bg_charcoal(Ansi.white(line))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def render_footer(empty: false, confirming: false)
|
|
44
|
+
if confirming
|
|
45
|
+
println Ansi.dim(" Cancel running pipeline? y/n")
|
|
46
|
+
elsif empty
|
|
47
|
+
println Ansi.dim(" q quit")
|
|
48
|
+
else
|
|
49
|
+
println Ansi.dim(" j/k move c cancel q quit")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def render_empty_state
|
|
54
|
+
println ""
|
|
55
|
+
println ""
|
|
56
|
+
println " No runs yet."
|
|
57
|
+
println ""
|
|
58
|
+
println " Trigger one: fun-ci trigger HEAD"
|
|
59
|
+
println " Or hook it: fun-ci install-hook pre-push"
|
|
60
|
+
println ""
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def render_board(rows, cursor_index:)
|
|
64
|
+
rows.each_with_index do |row, i|
|
|
65
|
+
if cursor_index == i
|
|
66
|
+
println "> #{row.lstrip}"
|
|
67
|
+
else
|
|
68
|
+
println row
|
|
69
|
+
end
|
|
70
|
+
println unless i == rows.length - 1
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def println(text = "")
|
|
75
|
+
@output.print "#{text}\e[K\r\n"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def clear
|
|
79
|
+
@output.print "\e[2J\e[H"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def move_cursor_home
|
|
83
|
+
@output.print "\e[H"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def clear_below
|
|
87
|
+
@output.print "\e[J"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def write_at(row, col, text)
|
|
91
|
+
@output.print "\e[#{row};#{col}H#{text}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def save_cursor
|
|
95
|
+
@output.print "\e[s"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def restore_cursor
|
|
99
|
+
@output.print "\e[u"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Tui
|
|
5
|
+
class Spinner
|
|
6
|
+
BRAILLE_FRAMES = %W[\u2800 \u2801 \u2803 \u2807 \u280F \u281F \u283F \u287F].freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :frames
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@frames = BRAILLE_FRAMES
|
|
12
|
+
@index = 0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def current_frame
|
|
16
|
+
@frames[@index]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def advance!
|
|
20
|
+
@index = (@index + 1) % @frames.length
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Tui
|
|
5
|
+
module StageChangeDetector
|
|
6
|
+
Change = Data.define(:run_id, :stage, :from, :to)
|
|
7
|
+
|
|
8
|
+
def self.detect(previous_runs, current_runs)
|
|
9
|
+
return [] if previous_runs.empty?
|
|
10
|
+
|
|
11
|
+
current_by_id = index_by_id(current_runs)
|
|
12
|
+
changes = []
|
|
13
|
+
|
|
14
|
+
previous_runs.each do |prev_run|
|
|
15
|
+
cur_run = current_by_id[prev_run[:id]]
|
|
16
|
+
next unless cur_run
|
|
17
|
+
|
|
18
|
+
changes.concat(compare_stages(prev_run, cur_run))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
changes
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.index_by_id(runs)
|
|
25
|
+
runs.each_with_object({}) { |r, h| h[r[:id]] = r }
|
|
26
|
+
end
|
|
27
|
+
private_class_method :index_by_id
|
|
28
|
+
|
|
29
|
+
def self.compare_stages(prev_run, cur_run)
|
|
30
|
+
prev_stages = index_stages(prev_run[:stages])
|
|
31
|
+
changes = []
|
|
32
|
+
|
|
33
|
+
cur_run[:stages].each do |cur_stage|
|
|
34
|
+
prev_stage = prev_stages[cur_stage[:stage]]
|
|
35
|
+
prev_status = prev_stage ? prev_stage[:status] : nil
|
|
36
|
+
next if prev_status == cur_stage[:status]
|
|
37
|
+
|
|
38
|
+
changes << Change.new(
|
|
39
|
+
run_id: cur_run[:id],
|
|
40
|
+
stage: cur_stage[:stage],
|
|
41
|
+
from: prev_status,
|
|
42
|
+
to: cur_stage[:status]
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
changes
|
|
47
|
+
end
|
|
48
|
+
private_class_method :compare_stages
|
|
49
|
+
|
|
50
|
+
def self.index_stages(stages)
|
|
51
|
+
return {} unless stages
|
|
52
|
+
|
|
53
|
+
stages.each_with_object({}) { |s, h| h[s[:stage]] = s }
|
|
54
|
+
end
|
|
55
|
+
private_class_method :index_stages
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Tui
|
|
5
|
+
module StreakCounter
|
|
6
|
+
PASS_STATUS = "completed"
|
|
7
|
+
ACTIVE_STATUSES = %w[running scheduled].freeze
|
|
8
|
+
|
|
9
|
+
def self.count(runs)
|
|
10
|
+
terminal_runs = runs.reject { |r| ACTIVE_STATUSES.include?(r[:status]) }
|
|
11
|
+
return nil if terminal_runs.empty?
|
|
12
|
+
|
|
13
|
+
streak = 0
|
|
14
|
+
terminal_runs.each do |run|
|
|
15
|
+
break unless run[:status] == PASS_STATUS
|
|
16
|
+
streak += 1
|
|
17
|
+
end
|
|
18
|
+
streak
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.format_text(streak)
|
|
22
|
+
return nil if streak.nil?
|
|
23
|
+
return "Streak broken" if streak == 0
|
|
24
|
+
|
|
25
|
+
"#{streak} in a row!"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Tui
|
|
5
|
+
class TerminalInput
|
|
6
|
+
def initialize(input:, width_provider: nil)
|
|
7
|
+
@input = input
|
|
8
|
+
@width_provider = width_provider
|
|
9
|
+
@signal_read = nil
|
|
10
|
+
@signal_write = nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def setup_signal_trap
|
|
14
|
+
return unless @width_provider
|
|
15
|
+
|
|
16
|
+
@signal_read, @signal_write = IO.pipe
|
|
17
|
+
Signal.trap("WINCH") do
|
|
18
|
+
@signal_write&.write_nonblock(".") rescue nil # rubocop:disable Style/RescueModifier
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def check_width
|
|
23
|
+
return nil unless @width_provider
|
|
24
|
+
|
|
25
|
+
@width_provider.call
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def setup_raw_mode
|
|
29
|
+
@input.raw! if @input.respond_to?(:raw!)
|
|
30
|
+
rescue Errno::ENOTTY
|
|
31
|
+
# Not a terminal (testing)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def restore_terminal
|
|
35
|
+
@input.cooked! if @input.respond_to?(:cooked!)
|
|
36
|
+
rescue Errno::ENOTTY
|
|
37
|
+
# Not a terminal
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def read_key_with_timeout(timeout)
|
|
41
|
+
return nil unless @input.respond_to?(:read_nonblock)
|
|
42
|
+
|
|
43
|
+
watched = [@input]
|
|
44
|
+
watched << @signal_read if @signal_read
|
|
45
|
+
ready = IO.select(watched, nil, nil, timeout)
|
|
46
|
+
return nil unless ready
|
|
47
|
+
|
|
48
|
+
if @signal_read && ready[0].include?(@signal_read)
|
|
49
|
+
@signal_read.read_nonblock(1) rescue nil # rubocop:disable Style/RescueModifier
|
|
50
|
+
return nil unless ready[0].include?(@input)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
byte = @input.read_nonblock(1)
|
|
54
|
+
if byte == "\e"
|
|
55
|
+
seq = @input.read_nonblock(2) rescue "" # rubocop:disable Style/RescueModifier
|
|
56
|
+
case seq
|
|
57
|
+
when "[A" then :up
|
|
58
|
+
when "[B" then :down
|
|
59
|
+
else :escape
|
|
60
|
+
end
|
|
61
|
+
else
|
|
62
|
+
byte
|
|
63
|
+
end
|
|
64
|
+
rescue IO::WaitReadable, EOFError
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/lib/fun_ci.rb
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "fun_ci/database"
|
|
4
|
-
require_relative "fun_ci/state_machine"
|
|
5
|
-
require_relative "fun_ci/pipeline_run"
|
|
6
|
-
require_relative "fun_ci/stage_job"
|
|
7
|
-
require_relative "fun_ci/project_config"
|
|
3
|
+
require_relative "fun_ci/persistence/database"
|
|
4
|
+
require_relative "fun_ci/persistence/state_machine"
|
|
5
|
+
require_relative "fun_ci/persistence/pipeline_run"
|
|
6
|
+
require_relative "fun_ci/persistence/stage_job"
|
|
7
|
+
require_relative "fun_ci/setup/project_config"
|
|
8
8
|
|
|
9
9
|
module FunCi
|
|
10
|
-
VERSION = "1.
|
|
10
|
+
VERSION = "1.2.0"
|
|
11
11
|
|
|
12
12
|
BUILD_TIMEOUT = 30
|
|
13
13
|
FAST_SUITE_TIMEOUT = 10
|