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
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "pipeline_run"
|
|
4
|
-
require_relative "stage_job"
|
|
5
|
-
|
|
6
|
-
module FunCi
|
|
7
|
-
class NullRecorder
|
|
8
|
-
def create_run(commit_hash:, branch:, project_path: nil) = nil
|
|
9
|
-
def start_stage(stage) = nil
|
|
10
|
-
def end_stage(job_id, status) = nil
|
|
11
|
-
def complete_run = nil
|
|
12
|
-
def fail_run = nil
|
|
13
|
-
def db = nil
|
|
14
|
-
def db_path = nil
|
|
15
|
-
def pipeline_run_id = nil
|
|
16
|
-
def close = nil
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
class DbRecorder
|
|
20
|
-
attr_reader :db, :db_path, :pipeline_run_id
|
|
21
|
-
|
|
22
|
-
def self.for_background(db_path, pipeline_run_id)
|
|
23
|
-
db = Database.connection(db_path)
|
|
24
|
-
new(db, pipeline_run_id: pipeline_run_id)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def initialize(db, pipeline_run_id: nil)
|
|
28
|
-
@db = db
|
|
29
|
-
@db_path = db.filename("main")
|
|
30
|
-
@pipeline_run_id = pipeline_run_id
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def close
|
|
34
|
-
@db.close rescue nil
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def create_run(commit_hash:, branch:, project_path: nil)
|
|
38
|
-
@pipeline_run_id = PipelineRun.create(@db, commit_hash: commit_hash, branch: branch, project_path: project_path)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def start_stage(stage)
|
|
42
|
-
return nil unless @pipeline_run_id
|
|
43
|
-
ensure_running
|
|
44
|
-
job_id = StageJob.create(@db, pipeline_run_id: @pipeline_run_id, stage: stage)
|
|
45
|
-
StageJob.update_status(@db, job_id, "running")
|
|
46
|
-
job_id
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def end_stage(job_id, status)
|
|
50
|
-
return unless job_id
|
|
51
|
-
StageJob.update_status(@db, job_id, status)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def complete_run
|
|
55
|
-
return unless @pipeline_run_id
|
|
56
|
-
PipelineRun.update_status(@db, @pipeline_run_id, "completed")
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def fail_run
|
|
60
|
-
return unless @pipeline_run_id
|
|
61
|
-
PipelineRun.update_status(@db, @pipeline_run_id, "failed")
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
private
|
|
65
|
-
|
|
66
|
-
def ensure_running
|
|
67
|
-
run = PipelineRun.find(@db, @pipeline_run_id)
|
|
68
|
-
return unless run && run[:status] == "scheduled"
|
|
69
|
-
PipelineRun.update_status(@db, @pipeline_run_id, "running")
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
data/lib/fun_ci/pipeline_run.rb
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "time"
|
|
4
|
-
|
|
5
|
-
module FunCi
|
|
6
|
-
module PipelineRun
|
|
7
|
-
def self.create(db, commit_hash:, branch:, project_path: nil)
|
|
8
|
-
now = Time.now.utc.iso8601
|
|
9
|
-
db.execute(
|
|
10
|
-
"INSERT INTO pipeline_runs (commit_hash, branch, project_path, status, created_at, updated_at) VALUES (?, ?, ?, 'scheduled', ?, ?)",
|
|
11
|
-
[commit_hash, branch, project_path, now, now]
|
|
12
|
-
)
|
|
13
|
-
db.last_insert_row_id
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def self.find(db, id)
|
|
17
|
-
row = db.execute("SELECT id, commit_hash, branch, status, pid, project_path, created_at, updated_at FROM pipeline_runs WHERE id = ?", [id]).first
|
|
18
|
-
return nil unless row
|
|
19
|
-
row_to_hash(row)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def self.find_by_branch(db, branch)
|
|
23
|
-
rows = db.execute("SELECT id, commit_hash, branch, status, pid, project_path, created_at, updated_at FROM pipeline_runs WHERE branch = ? ORDER BY id DESC", [branch])
|
|
24
|
-
rows.map { |row| row_to_hash(row) }
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def self.find_by_commit(db, commit_hash)
|
|
28
|
-
rows = db.execute("SELECT id, commit_hash, branch, status, pid, project_path, created_at, updated_at FROM pipeline_runs WHERE commit_hash = ? ORDER BY id DESC", [commit_hash])
|
|
29
|
-
rows.map { |row| row_to_hash(row) }
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def self.store_pid(db, id, pid)
|
|
33
|
-
db.execute("UPDATE pipeline_runs SET pid = ? WHERE id = ?", [pid, id])
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def self.find_running_with_pid(db, branch)
|
|
37
|
-
row = db.execute("SELECT id, commit_hash, branch, status, pid, project_path, created_at, updated_at FROM pipeline_runs WHERE branch = ? AND status = 'running' AND pid IS NOT NULL ORDER BY id DESC LIMIT 1", [branch]).first
|
|
38
|
-
return nil unless row
|
|
39
|
-
row_to_hash(row)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def self.update_status(db, id, new_status)
|
|
43
|
-
now = Time.now.utc.iso8601
|
|
44
|
-
db.execute("UPDATE pipeline_runs SET status = ?, updated_at = ? WHERE id = ?", [new_status, now, id])
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def self.recent(db, limit: 20)
|
|
48
|
-
rows = db.execute("SELECT id, commit_hash, branch, status, pid, project_path, created_at, updated_at FROM pipeline_runs ORDER BY id DESC LIMIT ?", [limit])
|
|
49
|
-
rows.map { |row| row_to_hash(row) }
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def self.row_to_hash(row)
|
|
53
|
-
{ id: row[0], commit_hash: row[1], branch: row[2], status: row[3], pid: row[4], project_path: row[5], created_at: row[6], updated_at: row[7] }
|
|
54
|
-
end
|
|
55
|
-
private_class_method :row_to_hash
|
|
56
|
-
end
|
|
57
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module FunCi
|
|
4
|
-
class ProgressReporter
|
|
5
|
-
def initialize(stdout:)
|
|
6
|
-
@stdout = stdout
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def phase_one_result(results)
|
|
10
|
-
markers = results.map { |stage, passed| "#{stage} #{result_marker(passed)}" }
|
|
11
|
-
summary = results.values.all? ? "phase 1 passed" : "phase 1 failed"
|
|
12
|
-
@stdout.puts "fun-ci: #{markers.join(" ")} (#{summary})"
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def fast_result(passed)
|
|
16
|
-
@stdout.puts "fun-ci: fast #{result_marker(passed)}"
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def slow_launched
|
|
20
|
-
@stdout.puts "fun-ci: slow (running in background)"
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
def result_marker(passed)
|
|
26
|
-
passed ? "ok" : "FAIL"
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module FunCi
|
|
4
|
-
class ProjectConfig
|
|
5
|
-
REQUIRED_SCRIPTS = %w[lint.sh build.sh fast.sh slow.sh].freeze
|
|
6
|
-
|
|
7
|
-
def initialize(project_root)
|
|
8
|
-
@project_root = project_root
|
|
9
|
-
@fun_ci_dir = File.join(project_root, ".fun-ci")
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def folder_exists?
|
|
13
|
-
Dir.exist?(@fun_ci_dir)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def validate
|
|
17
|
-
errors = []
|
|
18
|
-
|
|
19
|
-
unless folder_exists?
|
|
20
|
-
errors << "No .fun-ci/ folder found in #{@project_root}"
|
|
21
|
-
return errors
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
REQUIRED_SCRIPTS.each do |script|
|
|
25
|
-
path = File.join(@fun_ci_dir, script)
|
|
26
|
-
if !File.exist?(path)
|
|
27
|
-
errors << ".fun-ci/#{script} is not found"
|
|
28
|
-
elsif !File.executable?(path)
|
|
29
|
-
errors << ".fun-ci/#{script} is not executable"
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
errors
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def script_path(stage)
|
|
37
|
-
File.join(@fun_ci_dir, "#{stage}.sh")
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module FunCi
|
|
4
|
-
class ProjectDetector
|
|
5
|
-
def initialize(filenames)
|
|
6
|
-
@filenames = filenames
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def detect
|
|
10
|
-
return :ruby_bundler if @filenames.include?("Gemfile")
|
|
11
|
-
return :jvm_gradle_kotlin if @filenames.include?("build.gradle.kts")
|
|
12
|
-
return :jvm_gradle_groovy if @filenames.include?("build.gradle")
|
|
13
|
-
return :jvm_maven if @filenames.include?("pom.xml")
|
|
14
|
-
|
|
15
|
-
:unknown
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
data/lib/fun_ci/relative_time.rb
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "time"
|
|
4
|
-
|
|
5
|
-
module FunCi
|
|
6
|
-
module RelativeTime
|
|
7
|
-
def self.format(iso_timestamp, now: Time.now)
|
|
8
|
-
elapsed = now - Time.parse(iso_timestamp)
|
|
9
|
-
seconds = elapsed.to_i
|
|
10
|
-
|
|
11
|
-
if seconds < 60
|
|
12
|
-
"just now"
|
|
13
|
-
elsif seconds < 3600
|
|
14
|
-
"#{seconds / 60}m ago"
|
|
15
|
-
else
|
|
16
|
-
"#{seconds / 3600}h ago"
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
data/lib/fun_ci/row_formatter.rb
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
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 RowFormatter
|
|
9
|
-
STAGE_NAMES = { "lint" => "Lint", "build" => "Build", "fast" => "Fast", "slow" => "Slow" }.freeze
|
|
10
|
-
STATUS_LABELS = {
|
|
11
|
-
"completed" => "PASSED", "failed" => "FAILED", "timed_out" => "TIMED OUT",
|
|
12
|
-
"running" => "RUNNING", "scheduled" => "Scheduled...", "cancelled" => "CANCELLED"
|
|
13
|
-
}.freeze
|
|
14
|
-
PROJECT_COLORS = %w[31 32 33 34 35 36 91 92 93 94].freeze
|
|
15
|
-
|
|
16
|
-
def self.format(run, now: Time.now, spinner_frame: nil, elapsed_seconds: nil)
|
|
17
|
-
commit = run[:commit_hash][0, 7]
|
|
18
|
-
branch = run[:branch]
|
|
19
|
-
status = run[:status]
|
|
20
|
-
project = format_project(run[:project_path])
|
|
21
|
-
|
|
22
|
-
case status
|
|
23
|
-
when "scheduled"
|
|
24
|
-
format_scheduled(commit, branch, project, run[:updated_at], now)
|
|
25
|
-
when "cancelled"
|
|
26
|
-
format_cancelled(commit, branch, project, run[:stages], run[:updated_at], now)
|
|
27
|
-
else
|
|
28
|
-
stages_text = format_stages(run[:stages], status, spinner_frame, elapsed_seconds)
|
|
29
|
-
status_text = format_status(status)
|
|
30
|
-
time_text = Ansi.dim(RelativeTime.format(run[:updated_at], now: now))
|
|
31
|
-
" #{commit} #{branch}#{project} #{stages_text} #{status_text} #{time_text}"
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def self.format_project(project_path)
|
|
36
|
-
return "" unless project_path
|
|
37
|
-
name = File.basename(project_path)
|
|
38
|
-
code = PROJECT_COLORS[name.hash.abs % PROJECT_COLORS.size]
|
|
39
|
-
" \e[#{code}m#{name}#{Ansi::RESET}"
|
|
40
|
-
end
|
|
41
|
-
private_class_method :format_project
|
|
42
|
-
|
|
43
|
-
def self.format_scheduled(commit, branch, project, updated_at, now)
|
|
44
|
-
time_text = RelativeTime.format(updated_at, now: now)
|
|
45
|
-
Ansi.dim(" #{commit} #{branch}#{project} Scheduled... #{time_text}")
|
|
46
|
-
end
|
|
47
|
-
private_class_method :format_scheduled
|
|
48
|
-
|
|
49
|
-
def self.format_cancelled(commit, branch, project, stages, updated_at, now)
|
|
50
|
-
stages_text = stages.map { |s| format_cancelled_stage(s) }.join(" ")
|
|
51
|
-
time_text = RelativeTime.format(updated_at, now: now)
|
|
52
|
-
Ansi.dim(" #{commit} #{branch}#{project} #{stages_text} CANCELLED #{time_text}")
|
|
53
|
-
end
|
|
54
|
-
private_class_method :format_cancelled
|
|
55
|
-
|
|
56
|
-
def self.format_cancelled_stage(stage)
|
|
57
|
-
name = STAGE_NAMES[stage[:stage]]
|
|
58
|
-
if stage[:duration]
|
|
59
|
-
"#{name} #{DurationFormatter.format(stage[:duration])}"
|
|
60
|
-
else
|
|
61
|
-
"#{name} --"
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
private_class_method :format_cancelled_stage
|
|
65
|
-
|
|
66
|
-
def self.format_stages(stages, run_status, spinner_frame, elapsed_seconds)
|
|
67
|
-
stages.map { |s| format_stage(s, spinner_frame, elapsed_seconds) }.join(" ")
|
|
68
|
-
end
|
|
69
|
-
private_class_method :format_stages
|
|
70
|
-
|
|
71
|
-
def self.format_stage(stage, spinner_frame, elapsed_seconds)
|
|
72
|
-
name = STAGE_NAMES[stage[:stage]]
|
|
73
|
-
|
|
74
|
-
case stage[:status]
|
|
75
|
-
when "completed"
|
|
76
|
-
time = DurationFormatter.format(stage[:duration])
|
|
77
|
-
Ansi.green("#{name} #{time}")
|
|
78
|
-
when "failed"
|
|
79
|
-
time = DurationFormatter.format(stage[:duration])
|
|
80
|
-
Ansi.bold_red("#{name} FAIL #{time}")
|
|
81
|
-
when "timed_out"
|
|
82
|
-
time = DurationFormatter.format(stage[:duration])
|
|
83
|
-
Ansi.bold_yellow("#{name} TIMEOUT #{time}")
|
|
84
|
-
when "running"
|
|
85
|
-
frame = spinner_frame || "\u2800"
|
|
86
|
-
time = elapsed_seconds ? "#{elapsed_seconds.to_i}s" : "--"
|
|
87
|
-
Ansi.cyan("#{name} #{frame} #{time}")
|
|
88
|
-
else
|
|
89
|
-
Ansi.dim("#{name} --")
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
private_class_method :format_stage
|
|
93
|
-
|
|
94
|
-
def self.format_status(status)
|
|
95
|
-
label = STATUS_LABELS[status]
|
|
96
|
-
case status
|
|
97
|
-
when "completed" then Ansi.bold_green(label)
|
|
98
|
-
when "failed" then Ansi.bold_red(label)
|
|
99
|
-
when "timed_out" then Ansi.bold_yellow(label)
|
|
100
|
-
when "running" then Ansi.bold_cyan(label)
|
|
101
|
-
else Ansi.dim(label)
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
private_class_method :format_status
|
|
105
|
-
end
|
|
106
|
-
end
|
data/lib/fun_ci/screen.rb
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "ansi"
|
|
4
|
-
|
|
5
|
-
module FunCi
|
|
6
|
-
class Screen
|
|
7
|
-
attr_reader :width
|
|
8
|
-
|
|
9
|
-
def initialize(output: $stdout, width: 80)
|
|
10
|
-
@output = output
|
|
11
|
-
@width = width
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def width=(new_width)
|
|
15
|
-
return if new_width == @width
|
|
16
|
-
|
|
17
|
-
clear
|
|
18
|
-
@width = new_width
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def render_header(streak_text:)
|
|
22
|
-
title = " fun-ci"
|
|
23
|
-
if streak_text
|
|
24
|
-
streak_display = Ansi.green(streak_text)
|
|
25
|
-
plain_streak = streak_text
|
|
26
|
-
padding = @width - title.length - plain_streak.length
|
|
27
|
-
padding = 1 if padding < 1
|
|
28
|
-
line = "#{title}#{" " * padding}#{streak_display}"
|
|
29
|
-
else
|
|
30
|
-
line = title.ljust(@width)
|
|
31
|
-
end
|
|
32
|
-
println Ansi.bg_charcoal(Ansi.white(line))
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def render_footer(empty: false, confirming: false)
|
|
36
|
-
if confirming
|
|
37
|
-
println Ansi.dim(" Cancel running pipeline? y/n")
|
|
38
|
-
elsif empty
|
|
39
|
-
println Ansi.dim(" q quit")
|
|
40
|
-
else
|
|
41
|
-
println Ansi.dim(" j/k move c cancel q quit")
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def render_empty_state
|
|
46
|
-
println ""
|
|
47
|
-
println ""
|
|
48
|
-
println " No runs yet."
|
|
49
|
-
println ""
|
|
50
|
-
println " Trigger one: fun-ci trigger HEAD"
|
|
51
|
-
println " Or hook it: fun-ci install-hook pre-push"
|
|
52
|
-
println ""
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def render_board(rows, cursor_index:)
|
|
56
|
-
rows.each_with_index do |row, i|
|
|
57
|
-
if cursor_index == i
|
|
58
|
-
# Replace leading spaces with cursor marker
|
|
59
|
-
println "> #{row.lstrip}"
|
|
60
|
-
else
|
|
61
|
-
println row
|
|
62
|
-
end
|
|
63
|
-
println unless i == rows.length - 1
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def println(text = "")
|
|
68
|
-
@output.print "#{text}\e[K\r\n"
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def clear
|
|
72
|
-
@output.print "\e[2J\e[H"
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def move_cursor_home
|
|
76
|
-
@output.print "\e[H"
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def clear_below
|
|
80
|
-
@output.print "\e[J"
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def write_at(row, col, text)
|
|
84
|
-
@output.print "\e[#{row};#{col}H#{text}"
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def save_cursor
|
|
88
|
-
@output.print "\e[s"
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def restore_cursor
|
|
92
|
-
@output.print "\e[u"
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
data/lib/fun_ci/setup_checker.rb
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "project_config"
|
|
4
|
-
|
|
5
|
-
module FunCi
|
|
6
|
-
class SetupChecker
|
|
7
|
-
def self.run(project_root:, stdout: $stdout, stderr: $stderr)
|
|
8
|
-
new(project_root: project_root, stdout: stdout).run
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def initialize(project_root:, stdout:)
|
|
12
|
-
@config = ProjectConfig.new(project_root)
|
|
13
|
-
@stdout = stdout
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def run
|
|
17
|
-
errors = @config.validate
|
|
18
|
-
|
|
19
|
-
if errors.empty?
|
|
20
|
-
@stdout.puts "All OK — project is configured."
|
|
21
|
-
0
|
|
22
|
-
else
|
|
23
|
-
errors.each { |e| @stdout.puts e }
|
|
24
|
-
1
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
data/lib/fun_ci/spinner.rb
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module FunCi
|
|
4
|
-
class Spinner
|
|
5
|
-
BRAILLE_FRAMES = %W[\u2800 \u2801 \u2803 \u2807 \u280F \u281F \u283F \u287F].freeze
|
|
6
|
-
|
|
7
|
-
attr_reader :frames
|
|
8
|
-
|
|
9
|
-
def initialize
|
|
10
|
-
@frames = BRAILLE_FRAMES
|
|
11
|
-
@index = 0
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def current_frame
|
|
15
|
-
@frames[@index]
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def advance!
|
|
19
|
-
@index = (@index + 1) % @frames.length
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module FunCi
|
|
4
|
-
module StageChangeDetector
|
|
5
|
-
Change = Data.define(:run_id, :stage, :from, :to)
|
|
6
|
-
|
|
7
|
-
def self.detect(previous_runs, current_runs)
|
|
8
|
-
return [] if previous_runs.empty?
|
|
9
|
-
|
|
10
|
-
current_by_id = index_by_id(current_runs)
|
|
11
|
-
changes = []
|
|
12
|
-
|
|
13
|
-
previous_runs.each do |prev_run|
|
|
14
|
-
cur_run = current_by_id[prev_run[:id]]
|
|
15
|
-
next unless cur_run
|
|
16
|
-
|
|
17
|
-
changes.concat(compare_stages(prev_run, cur_run))
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
changes
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def self.index_by_id(runs)
|
|
24
|
-
runs.each_with_object({}) { |r, h| h[r[:id]] = r }
|
|
25
|
-
end
|
|
26
|
-
private_class_method :index_by_id
|
|
27
|
-
|
|
28
|
-
def self.compare_stages(prev_run, cur_run)
|
|
29
|
-
prev_stages = index_stages(prev_run[:stages])
|
|
30
|
-
changes = []
|
|
31
|
-
|
|
32
|
-
cur_run[:stages].each do |cur_stage|
|
|
33
|
-
prev_stage = prev_stages[cur_stage[:stage]]
|
|
34
|
-
prev_status = prev_stage ? prev_stage[:status] : nil
|
|
35
|
-
next if prev_status == cur_stage[:status]
|
|
36
|
-
|
|
37
|
-
changes << Change.new(
|
|
38
|
-
run_id: cur_run[:id],
|
|
39
|
-
stage: cur_stage[:stage],
|
|
40
|
-
from: prev_status,
|
|
41
|
-
to: cur_stage[:status]
|
|
42
|
-
)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
changes
|
|
46
|
-
end
|
|
47
|
-
private_class_method :compare_stages
|
|
48
|
-
|
|
49
|
-
def self.index_stages(stages)
|
|
50
|
-
return {} unless stages
|
|
51
|
-
|
|
52
|
-
stages.each_with_object({}) { |s, h| h[s[:stage]] = s }
|
|
53
|
-
end
|
|
54
|
-
private_class_method :index_stages
|
|
55
|
-
end
|
|
56
|
-
end
|
data/lib/fun_ci/stage_job.rb
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "time"
|
|
4
|
-
|
|
5
|
-
module FunCi
|
|
6
|
-
module StageJob
|
|
7
|
-
TERMINAL_STATUSES = %w[completed failed timed_out cancelled].freeze
|
|
8
|
-
|
|
9
|
-
def self.create(db, pipeline_run_id:, stage:)
|
|
10
|
-
db.execute(
|
|
11
|
-
"INSERT INTO stage_jobs (pipeline_run_id, stage, status) VALUES (?, ?, 'scheduled')",
|
|
12
|
-
[pipeline_run_id, stage]
|
|
13
|
-
)
|
|
14
|
-
db.last_insert_row_id
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def self.find(db, id)
|
|
18
|
-
row = db.execute("SELECT id, pipeline_run_id, stage, status, started_at, completed_at FROM stage_jobs WHERE id = ?", [id]).first
|
|
19
|
-
return nil unless row
|
|
20
|
-
row_to_hash(row)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def self.update_status(db, id, new_status)
|
|
24
|
-
now = Time.now.utc.iso8601
|
|
25
|
-
if new_status == "running"
|
|
26
|
-
db.execute("UPDATE stage_jobs SET status = ?, started_at = ? WHERE id = ?", [new_status, now, id])
|
|
27
|
-
elsif TERMINAL_STATUSES.include?(new_status)
|
|
28
|
-
db.execute("UPDATE stage_jobs SET status = ?, completed_at = ? WHERE id = ?", [new_status, now, id])
|
|
29
|
-
else
|
|
30
|
-
db.execute("UPDATE stage_jobs SET status = ? WHERE id = ?", [new_status, id])
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def self.elapsed_duration(job)
|
|
35
|
-
return nil unless job[:started_at] && job[:completed_at]
|
|
36
|
-
Time.parse(job[:completed_at]) - Time.parse(job[:started_at])
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def self.row_to_hash(row)
|
|
40
|
-
{ id: row[0], pipeline_run_id: row[1], stage: row[2], status: row[3], started_at: row[4], completed_at: row[5] }
|
|
41
|
-
end
|
|
42
|
-
private_class_method :row_to_hash
|
|
43
|
-
end
|
|
44
|
-
end
|