fun_ci 1.0.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 +34 -0
- data/README.md +3 -1
- data/exe/fun-ci-trigger +6 -6
- data/exe/fun-ci-tui +9 -4
- data/lib/fun_ci/animations/celebrate.rb +210 -0
- data/lib/fun_ci/animations/explosion.rb +138 -0
- data/lib/fun_ci/animations/flash.rb +205 -0
- data/lib/fun_ci/animations/idle.rb +127 -0
- data/lib/fun_ci/animations/leprechauns.rb +240 -0
- data/lib/fun_ci/animations/running.rb +129 -0
- data/lib/fun_ci/animations/success.rb +281 -0
- data/lib/fun_ci/animations/yay.rb +169 -0
- data/lib/fun_ci/cli.rb +20 -14
- 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 +49 -28
- data/lib/fun_ci/admin_tui.rb +0 -226
- data/lib/fun_ci/ansi.rb +0 -21
- 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/hook_writer.rb +0 -73
- data/lib/fun_ci/installer.rb +0 -53
- 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 -81
- data/lib/fun_ci/setup_checker.rb +0 -28
- data/lib/fun_ci/spinner.rb +0 -22
- 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
data/lib/fun_ci/admin_tui.rb
DELETED
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "board_data"
|
|
4
|
-
require_relative "row_formatter"
|
|
5
|
-
require_relative "screen"
|
|
6
|
-
require_relative "spinner"
|
|
7
|
-
require_relative "streak_counter"
|
|
8
|
-
require "io/console"
|
|
9
|
-
|
|
10
|
-
module FunCi
|
|
11
|
-
class AdminTui
|
|
12
|
-
FAST_REFRESH = 0.1 # seconds (spinner + timer)
|
|
13
|
-
SLOW_REFRESH = 5.0 # seconds (settled board)
|
|
14
|
-
|
|
15
|
-
def initialize(db:, output: $stdout, input: $stdin, width: 80, width_provider: nil, page_size: nil)
|
|
16
|
-
@board_data = BoardData.new(db, page_size: page_size)
|
|
17
|
-
@screen = Screen.new(output: output, width: width)
|
|
18
|
-
@output = output
|
|
19
|
-
@input = input
|
|
20
|
-
@width_provider = width_provider
|
|
21
|
-
@spinner = Spinner.new
|
|
22
|
-
@cursor_index = nil
|
|
23
|
-
@running = false
|
|
24
|
-
@confirm_cancel = nil
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def render_once
|
|
28
|
-
update_width_from_provider
|
|
29
|
-
runs = @board_data.runs
|
|
30
|
-
streak = @board_data.streak
|
|
31
|
-
streak_text = StreakCounter.format_text(streak)
|
|
32
|
-
|
|
33
|
-
@screen.render_header(streak_text: streak_text)
|
|
34
|
-
@screen.println
|
|
35
|
-
|
|
36
|
-
if runs.empty?
|
|
37
|
-
@screen.render_empty_state
|
|
38
|
-
@screen.render_footer(empty: true)
|
|
39
|
-
else
|
|
40
|
-
rows = runs.map { |run| format_run(run) }
|
|
41
|
-
@screen.render_board(rows, cursor_index: @cursor_index)
|
|
42
|
-
@screen.println
|
|
43
|
-
@screen.render_footer(empty: false, confirming: confirming?)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
@screen.clear_below
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def run
|
|
50
|
-
@running = true
|
|
51
|
-
@screen.clear
|
|
52
|
-
|
|
53
|
-
begin
|
|
54
|
-
setup_raw_mode
|
|
55
|
-
setup_sigwinch_trap
|
|
56
|
-
loop do
|
|
57
|
-
render_frame
|
|
58
|
-
break unless @running
|
|
59
|
-
key = read_key_with_timeout(refresh_interval)
|
|
60
|
-
handle_key(key) if key
|
|
61
|
-
end
|
|
62
|
-
ensure
|
|
63
|
-
restore_terminal
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def resize(new_width)
|
|
68
|
-
@screen.width = new_width
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def confirming?
|
|
72
|
-
!@confirm_cancel.nil?
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def confirmation_run
|
|
76
|
-
@confirm_cancel
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def handle_key(key)
|
|
80
|
-
if @confirm_cancel
|
|
81
|
-
handle_confirm_key(key)
|
|
82
|
-
return
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
case key
|
|
86
|
-
when "q"
|
|
87
|
-
@running = false
|
|
88
|
-
when "j", :down
|
|
89
|
-
move_cursor_down
|
|
90
|
-
when "k", :up
|
|
91
|
-
move_cursor_up
|
|
92
|
-
when "c"
|
|
93
|
-
initiate_cancel
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
private
|
|
98
|
-
|
|
99
|
-
def format_run(run)
|
|
100
|
-
opts = {}
|
|
101
|
-
if run[:status] == "running"
|
|
102
|
-
active_stage = run[:stages].find { |s| s[:status] == "running" }
|
|
103
|
-
if active_stage && active_stage[:started_at]
|
|
104
|
-
opts[:elapsed_seconds] = Time.now - Time.parse(active_stage[:started_at])
|
|
105
|
-
end
|
|
106
|
-
opts[:spinner_frame] = @spinner.current_frame
|
|
107
|
-
end
|
|
108
|
-
RowFormatter.format(run, **opts)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def render_frame
|
|
112
|
-
@screen.move_cursor_home
|
|
113
|
-
@spinner.advance!
|
|
114
|
-
render_once
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def refresh_interval
|
|
118
|
-
runs = @board_data.runs
|
|
119
|
-
any_running = runs.any? { |r| r[:status] == "running" }
|
|
120
|
-
any_running ? FAST_REFRESH : SLOW_REFRESH
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def move_cursor_down
|
|
124
|
-
runs = @board_data.runs
|
|
125
|
-
return if runs.empty?
|
|
126
|
-
|
|
127
|
-
if @cursor_index.nil?
|
|
128
|
-
@cursor_index = 0
|
|
129
|
-
elsif @cursor_index < runs.length - 1
|
|
130
|
-
@cursor_index += 1
|
|
131
|
-
@board_data.load_more if @cursor_index == runs.length - 1
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def move_cursor_up
|
|
136
|
-
return if @cursor_index.nil?
|
|
137
|
-
|
|
138
|
-
if @cursor_index > 0
|
|
139
|
-
@cursor_index -= 1
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def initiate_cancel
|
|
144
|
-
return if @cursor_index.nil?
|
|
145
|
-
|
|
146
|
-
runs = @board_data.runs
|
|
147
|
-
return if @cursor_index >= runs.length
|
|
148
|
-
|
|
149
|
-
run = runs[@cursor_index]
|
|
150
|
-
case run[:status]
|
|
151
|
-
when "scheduled"
|
|
152
|
-
@board_data.cancel_run(run[:id])
|
|
153
|
-
when "running"
|
|
154
|
-
@confirm_cancel = run
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def handle_confirm_key(key)
|
|
159
|
-
case key
|
|
160
|
-
when "y"
|
|
161
|
-
@board_data.cancel_run(@confirm_cancel[:id])
|
|
162
|
-
@confirm_cancel = nil
|
|
163
|
-
when "n", :escape
|
|
164
|
-
@confirm_cancel = nil
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def update_width_from_provider
|
|
169
|
-
return unless @width_provider
|
|
170
|
-
|
|
171
|
-
new_width = @width_provider.call
|
|
172
|
-
resize(new_width) if new_width
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def setup_sigwinch_trap
|
|
176
|
-
return unless @width_provider
|
|
177
|
-
|
|
178
|
-
@signal_read, @signal_write = IO.pipe
|
|
179
|
-
Signal.trap("WINCH") do
|
|
180
|
-
@signal_write.write_nonblock(".") rescue nil
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def setup_raw_mode
|
|
185
|
-
@input.raw! if @input.respond_to?(:raw!)
|
|
186
|
-
rescue Errno::ENOTTY
|
|
187
|
-
# Not a terminal (testing)
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def restore_terminal
|
|
191
|
-
@input.cooked! if @input.respond_to?(:cooked!)
|
|
192
|
-
rescue Errno::ENOTTY
|
|
193
|
-
# Not a terminal
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def read_key_with_timeout(timeout)
|
|
197
|
-
return nil unless @input.respond_to?(:read_nonblock)
|
|
198
|
-
|
|
199
|
-
watched = [@input]
|
|
200
|
-
watched << @signal_read if @signal_read
|
|
201
|
-
ready = IO.select(watched, nil, nil, timeout)
|
|
202
|
-
return nil unless ready
|
|
203
|
-
|
|
204
|
-
# Drain signal pipe if it woke us
|
|
205
|
-
if @signal_read && ready[0].include?(@signal_read)
|
|
206
|
-
@signal_read.read_nonblock(1) rescue nil
|
|
207
|
-
return nil unless ready[0].include?(@input)
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
byte = @input.read_nonblock(1)
|
|
211
|
-
if byte == "\e"
|
|
212
|
-
# Escape sequence
|
|
213
|
-
seq = @input.read_nonblock(2) rescue ""
|
|
214
|
-
case seq
|
|
215
|
-
when "[A" then :up
|
|
216
|
-
when "[B" then :down
|
|
217
|
-
else :escape
|
|
218
|
-
end
|
|
219
|
-
else
|
|
220
|
-
byte
|
|
221
|
-
end
|
|
222
|
-
rescue IO::WaitReadable, EOFError
|
|
223
|
-
nil
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
end
|
data/lib/fun_ci/ansi.rb
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module FunCi
|
|
4
|
-
module Ansi
|
|
5
|
-
RESET = "\e[0m"
|
|
6
|
-
|
|
7
|
-
def self.green(text) = "\e[32m#{text}#{RESET}"
|
|
8
|
-
def self.bold_green(text) = "\e[1;32m#{text}#{RESET}"
|
|
9
|
-
def self.bold_red(text) = "\e[1;31m#{text}#{RESET}"
|
|
10
|
-
def self.bold_yellow(text) = "\e[1;33m#{text}#{RESET}"
|
|
11
|
-
def self.cyan(text) = "\e[36m#{text}#{RESET}"
|
|
12
|
-
def self.bold_cyan(text) = "\e[1;36m#{text}#{RESET}"
|
|
13
|
-
def self.dim(text) = "\e[2m#{text}#{RESET}"
|
|
14
|
-
def self.white(text) = "\e[37m#{text}#{RESET}"
|
|
15
|
-
def self.bg_charcoal(text) = "\e[48;5;236m#{text}#{RESET}"
|
|
16
|
-
|
|
17
|
-
def self.strip(text)
|
|
18
|
-
text.gsub(/\e\[[0-9;]*[A-Za-z]/, "")
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "timeout"
|
|
4
|
-
|
|
5
|
-
module FunCi
|
|
6
|
-
class BackgroundWrapper
|
|
7
|
-
def initialize(recorder:, job_id:, executor:)
|
|
8
|
-
@recorder = recorder
|
|
9
|
-
@job_id = job_id
|
|
10
|
-
@executor = executor
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def run
|
|
14
|
-
_output, status = @executor.call
|
|
15
|
-
if status.success?
|
|
16
|
-
@recorder.end_stage(@job_id, "completed")
|
|
17
|
-
@recorder.complete_run
|
|
18
|
-
else
|
|
19
|
-
@recorder.end_stage(@job_id, "failed")
|
|
20
|
-
@recorder.fail_run
|
|
21
|
-
end
|
|
22
|
-
rescue Timeout::Error
|
|
23
|
-
@recorder.end_stage(@job_id, "timed_out")
|
|
24
|
-
@recorder.fail_run
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
data/lib/fun_ci/board_data.rb
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "time"
|
|
4
|
-
require_relative "pipeline_run"
|
|
5
|
-
require_relative "stage_job"
|
|
6
|
-
require_relative "streak_counter"
|
|
7
|
-
|
|
8
|
-
module FunCi
|
|
9
|
-
class BoardData
|
|
10
|
-
def initialize(db, limit: 15, page_size: nil)
|
|
11
|
-
@db = db
|
|
12
|
-
@page_size = page_size || limit
|
|
13
|
-
@limit = @page_size
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def load_more
|
|
17
|
-
@limit += @page_size
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def runs
|
|
21
|
-
pipeline_runs = PipelineRun.recent(@db, limit: @limit)
|
|
22
|
-
pipeline_runs.map { |run| enrich_with_stages(run) }
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def streak
|
|
26
|
-
pipeline_runs = PipelineRun.recent(@db, limit: @limit)
|
|
27
|
-
StreakCounter.count(pipeline_runs)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def cancel_run(run_id)
|
|
31
|
-
PipelineRun.update_status(@db, run_id, "cancelled")
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def enrich_with_stages(run)
|
|
37
|
-
rows = @db.execute(
|
|
38
|
-
"SELECT id, pipeline_run_id, stage, status, started_at, completed_at FROM stage_jobs WHERE pipeline_run_id = ? ORDER BY id",
|
|
39
|
-
[run[:id]]
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
stages = rows.map do |row|
|
|
43
|
-
job = { id: row[0], pipeline_run_id: row[1], stage: row[2], status: row[3], started_at: row[4], completed_at: row[5] }
|
|
44
|
-
duration = StageJob.elapsed_duration(job)
|
|
45
|
-
{ stage: job[:stage], status: job[:status], duration: duration, started_at: job[:started_at] }
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
run.merge(stages: stages)
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
data/lib/fun_ci/database.rb
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "sqlite3"
|
|
4
|
-
|
|
5
|
-
module FunCi
|
|
6
|
-
module Database
|
|
7
|
-
def self.connection(db_path)
|
|
8
|
-
db = SQLite3::Database.new(db_path)
|
|
9
|
-
db.busy_timeout = 5000
|
|
10
|
-
db.execute("PRAGMA journal_mode=WAL")
|
|
11
|
-
db.execute("PRAGMA foreign_keys=ON")
|
|
12
|
-
db
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.migrate!(db)
|
|
16
|
-
db.execute(<<~SQL)
|
|
17
|
-
CREATE TABLE IF NOT EXISTS pipeline_runs (
|
|
18
|
-
id INTEGER PRIMARY KEY,
|
|
19
|
-
commit_hash TEXT,
|
|
20
|
-
branch TEXT,
|
|
21
|
-
status TEXT DEFAULT 'scheduled',
|
|
22
|
-
pid INTEGER,
|
|
23
|
-
created_at TEXT,
|
|
24
|
-
updated_at TEXT
|
|
25
|
-
)
|
|
26
|
-
SQL
|
|
27
|
-
|
|
28
|
-
add_column_if_missing(db, "pipeline_runs", "pid", "INTEGER")
|
|
29
|
-
add_column_if_missing(db, "pipeline_runs", "project_path", "TEXT")
|
|
30
|
-
|
|
31
|
-
db.execute(<<~SQL)
|
|
32
|
-
CREATE TABLE IF NOT EXISTS stage_jobs (
|
|
33
|
-
id INTEGER PRIMARY KEY,
|
|
34
|
-
pipeline_run_id INTEGER REFERENCES pipeline_runs(id),
|
|
35
|
-
stage TEXT,
|
|
36
|
-
status TEXT DEFAULT 'scheduled',
|
|
37
|
-
started_at TEXT,
|
|
38
|
-
completed_at TEXT
|
|
39
|
-
)
|
|
40
|
-
SQL
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def self.add_column_if_missing(db, table, column, type)
|
|
44
|
-
columns = db.execute("PRAGMA table_info(#{table})").map { |row| row[1] }
|
|
45
|
-
return if columns.include?(column)
|
|
46
|
-
db.execute("ALTER TABLE #{table} ADD COLUMN #{column} #{type}")
|
|
47
|
-
end
|
|
48
|
-
private_class_method :add_column_if_missing
|
|
49
|
-
end
|
|
50
|
-
end
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module FunCi
|
|
4
|
-
module DurationFormatter
|
|
5
|
-
def self.format(seconds)
|
|
6
|
-
if seconds >= 60
|
|
7
|
-
mins = (seconds / 60).to_i
|
|
8
|
-
secs = (seconds % 60).to_i
|
|
9
|
-
"#{mins}m#{secs.to_s.rjust(2, "0")}"
|
|
10
|
-
elsif seconds == seconds.to_i
|
|
11
|
-
"#{seconds.to_i}s"
|
|
12
|
-
else
|
|
13
|
-
"#{format_decimal(seconds)}s"
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def self.format_decimal(value)
|
|
18
|
-
# Show one decimal place, remove trailing zeros
|
|
19
|
-
sprintf("%.1f", value)
|
|
20
|
-
end
|
|
21
|
-
private_class_method :format_decimal
|
|
22
|
-
end
|
|
23
|
-
end
|
data/lib/fun_ci/hook_writer.rb
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "fileutils"
|
|
4
|
-
|
|
5
|
-
module FunCi
|
|
6
|
-
class HookWriter
|
|
7
|
-
ALLOWED_HOOKS = %w[pre-commit pre-push].freeze
|
|
8
|
-
MARKER = "# fun-ci-managed-hook"
|
|
9
|
-
|
|
10
|
-
HOOK_COMMANDS = {
|
|
11
|
-
"pre-commit" => "fun-ci trigger --no-validate",
|
|
12
|
-
"pre-push" => "fun-ci trigger"
|
|
13
|
-
}.freeze
|
|
14
|
-
|
|
15
|
-
HOOK_TEMPLATE = <<~SH
|
|
16
|
-
#!/bin/sh
|
|
17
|
-
#{MARKER}
|
|
18
|
-
COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "0000000000000000000000000000000000000000")
|
|
19
|
-
BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
|
20
|
-
%<command>s "$COMMIT" "$BRANCH"
|
|
21
|
-
SH
|
|
22
|
-
|
|
23
|
-
def self.run(project_root:, hook_type:, stdout: $stdout)
|
|
24
|
-
new(project_root: project_root, hook_type: hook_type, stdout: stdout).run
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def initialize(project_root:, hook_type:, stdout:)
|
|
28
|
-
@project_root = project_root
|
|
29
|
-
@hook_type = hook_type
|
|
30
|
-
@stdout = stdout
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def run
|
|
34
|
-
return reject("Not a git repository — no .git/ found.") unless git_repo?
|
|
35
|
-
return reject("Unknown hook type: #{@hook_type}") unless ALLOWED_HOOKS.include?(@hook_type)
|
|
36
|
-
return skip("Hook #{@hook_type} already exists from another tool — skipping.") if foreign_hook?
|
|
37
|
-
|
|
38
|
-
write_hook
|
|
39
|
-
@stdout.puts "Installed #{@hook_type} hook."
|
|
40
|
-
0
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
def git_repo?
|
|
46
|
-
Dir.exist?(File.join(@project_root, ".git"))
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def hook_path
|
|
50
|
-
File.join(@project_root, ".git", "hooks", @hook_type)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def foreign_hook?
|
|
54
|
-
File.exist?(hook_path) && !File.read(hook_path).include?(MARKER)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def write_hook
|
|
58
|
-
FileUtils.mkdir_p(File.join(@project_root, ".git", "hooks"))
|
|
59
|
-
File.write(hook_path, format(HOOK_TEMPLATE, command: HOOK_COMMANDS[@hook_type]))
|
|
60
|
-
File.chmod(0o755, hook_path)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def reject(message)
|
|
64
|
-
@stdout.puts message
|
|
65
|
-
1
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def skip(message)
|
|
69
|
-
@stdout.puts message
|
|
70
|
-
0
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
data/lib/fun_ci/installer.rb
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "project_detector"
|
|
4
|
-
require_relative "template_writer"
|
|
5
|
-
require_relative "maven_linter_detector"
|
|
6
|
-
|
|
7
|
-
module FunCi
|
|
8
|
-
class Installer
|
|
9
|
-
def self.run(project_root:, stdout: $stdout, pom_reader: nil)
|
|
10
|
-
new(project_root: project_root, stdout: stdout, pom_reader: pom_reader).run
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def initialize(project_root:, stdout:, pom_reader: nil)
|
|
14
|
-
@project_root = project_root
|
|
15
|
-
@stdout = stdout
|
|
16
|
-
@pom_reader = pom_reader || ->(path) { File.read(path) }
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def run
|
|
20
|
-
if Dir.exist?(File.join(@project_root, ".fun-ci"))
|
|
21
|
-
@stdout.puts ".fun-ci/ already exists — skipping init."
|
|
22
|
-
return 0
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
filenames = Dir.children(@project_root)
|
|
26
|
-
detected = ProjectDetector.new(filenames).detect
|
|
27
|
-
|
|
28
|
-
if detected == :unknown
|
|
29
|
-
@stdout.puts "Could not detect project type. Create .fun-ci/ manually."
|
|
30
|
-
return 1
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
@stdout.puts "Detected: #{detected.to_s.tr("_", " ")}"
|
|
34
|
-
|
|
35
|
-
lint_override = detect_maven_linter(detected)
|
|
36
|
-
TemplateWriter.new(detected, @project_root, lint_override: lint_override).write
|
|
37
|
-
|
|
38
|
-
@stdout.puts "Created .fun-ci/ with template scripts."
|
|
39
|
-
0
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
def detect_maven_linter(detected)
|
|
45
|
-
return nil unless detected == :jvm_maven
|
|
46
|
-
|
|
47
|
-
pom_path = File.join(@project_root, "pom.xml")
|
|
48
|
-
pom_content = @pom_reader.call(pom_path)
|
|
49
|
-
command = MavenLinterDetector.new(pom_content).lint_command
|
|
50
|
-
command == MavenLinterDetector::DEFAULT_COMMAND ? nil : command
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module FunCi
|
|
4
|
-
class MavenLinterDetector
|
|
5
|
-
LINTERS = [
|
|
6
|
-
{ artifact_id: "detekt-maven-plugin", command: "mvn detekt:check" },
|
|
7
|
-
{ artifact_id: "ktlint-maven-plugin", command: "mvn ktlint:check" },
|
|
8
|
-
{ artifact_id: "maven-checkstyle-plugin", command: "mvn checkstyle:check" },
|
|
9
|
-
{ artifact_id: "spotbugs-maven-plugin", command: "mvn spotbugs:check" },
|
|
10
|
-
{ artifact_id: "maven-pmd-plugin", command: "mvn pmd:check" }
|
|
11
|
-
].freeze
|
|
12
|
-
|
|
13
|
-
DEFAULT_COMMAND = "mvn verify -DskipTests"
|
|
14
|
-
|
|
15
|
-
def initialize(pom_content)
|
|
16
|
-
@pom_content = pom_content
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def lint_command
|
|
20
|
-
match = LINTERS.find { |linter| @pom_content.include?(linter[:artifact_id]) }
|
|
21
|
-
match ? match[:command] : DEFAULT_COMMAND
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "database"
|
|
4
|
-
require_relative "pipeline_recorder"
|
|
5
|
-
require_relative "background_wrapper"
|
|
6
|
-
|
|
7
|
-
module FunCi
|
|
8
|
-
class PipelineForker
|
|
9
|
-
def self.fork_pipeline(commit_hash:, branch:, db_path:)
|
|
10
|
-
pid = fork do
|
|
11
|
-
run_in_child(commit_hash: commit_hash, branch: branch, db_path: db_path)
|
|
12
|
-
end
|
|
13
|
-
Process.detach(pid)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def self.run_in_child(commit_hash:, branch:, db_path:)
|
|
17
|
-
db = Database.connection(db_path)
|
|
18
|
-
recorder = DbRecorder.new(db)
|
|
19
|
-
Trigger.new(
|
|
20
|
-
project_root: Dir.pwd,
|
|
21
|
-
commit_hash: commit_hash,
|
|
22
|
-
branch: branch,
|
|
23
|
-
stdout: File.open(File::NULL, "w"),
|
|
24
|
-
recorder: recorder,
|
|
25
|
-
background_launcher: method(:sync_launcher)
|
|
26
|
-
).run
|
|
27
|
-
recorder.close
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def self.sync_launcher(db_path:, pipeline_run_id:, job_id:, executor:)
|
|
31
|
-
recorder = DbRecorder.for_background(db_path, pipeline_run_id)
|
|
32
|
-
BackgroundWrapper.new(recorder: recorder, job_id: job_id, executor: executor).run
|
|
33
|
-
recorder.close
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
@@ -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
|