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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/exe/fun-ci-trigger +6 -6
  4. data/exe/fun-ci-tui +6 -6
  5. data/lib/fun_ci/cli.rb +17 -16
  6. data/lib/fun_ci/persistence/database.rb +52 -0
  7. data/lib/fun_ci/persistence/pipeline_recorder.rb +74 -0
  8. data/lib/fun_ci/persistence/pipeline_run.rb +59 -0
  9. data/lib/fun_ci/persistence/stage_job.rb +46 -0
  10. data/lib/fun_ci/{state_machine.rb → persistence/state_machine.rb} +3 -12
  11. data/lib/fun_ci/pipeline/background_wrapper.rb +27 -0
  12. data/lib/fun_ci/pipeline/pipeline_forker.rb +38 -0
  13. data/lib/fun_ci/pipeline/process_runner.rb +28 -0
  14. data/lib/fun_ci/pipeline/progress_reporter.rb +31 -0
  15. data/lib/fun_ci/pipeline/stage_runner.rb +85 -0
  16. data/lib/fun_ci/pipeline/stale_pipeline_canceller.rb +53 -0
  17. data/lib/fun_ci/pipeline/trigger.rb +153 -0
  18. data/lib/fun_ci/setup/hook_writer.rb +75 -0
  19. data/lib/fun_ci/setup/installer.rb +55 -0
  20. data/lib/fun_ci/setup/maven_linter_detector.rb +26 -0
  21. data/lib/fun_ci/setup/project_config.rb +42 -0
  22. data/lib/fun_ci/setup/project_detector.rb +22 -0
  23. data/lib/fun_ci/setup/setup_checker.rb +30 -0
  24. data/lib/fun_ci/setup/template_writer.rb +53 -0
  25. data/lib/fun_ci/tui/admin_tui.rb +90 -0
  26. data/lib/fun_ci/tui/animation.rb +49 -0
  27. data/lib/fun_ci/tui/animation_compositor.rb +107 -0
  28. data/lib/fun_ci/tui/animation_frames.rb +112 -0
  29. data/lib/fun_ci/tui/animation_library.rb +46 -0
  30. data/lib/fun_ci/tui/animation_renderer.rb +144 -0
  31. data/lib/fun_ci/tui/ansi.rb +34 -0
  32. data/lib/fun_ci/tui/board_data.rb +53 -0
  33. data/lib/fun_ci/tui/board_renderer.rb +105 -0
  34. data/lib/fun_ci/tui/duration_formatter.rb +24 -0
  35. data/lib/fun_ci/tui/header_animation_manager.rb +71 -0
  36. data/lib/fun_ci/tui/header_animation_player.rb +45 -0
  37. data/lib/fun_ci/tui/key_handler.rb +86 -0
  38. data/lib/fun_ci/tui/looping_animation_player.rb +45 -0
  39. data/lib/fun_ci/tui/relative_time.rb +22 -0
  40. data/lib/fun_ci/tui/row_formatter.rb +108 -0
  41. data/lib/fun_ci/tui/screen.rb +103 -0
  42. data/lib/fun_ci/tui/spinner.rb +24 -0
  43. data/lib/fun_ci/tui/stage_change_detector.rb +58 -0
  44. data/lib/fun_ci/tui/streak_counter.rb +29 -0
  45. data/lib/fun_ci/tui/terminal_input.rb +69 -0
  46. data/lib/fun_ci.rb +6 -6
  47. metadata +41 -37
  48. data/lib/fun_ci/admin_tui.rb +0 -238
  49. data/lib/fun_ci/animation.rb +0 -47
  50. data/lib/fun_ci/animation_compositor.rb +0 -105
  51. data/lib/fun_ci/animation_frames.rb +0 -111
  52. data/lib/fun_ci/animation_library.rb +0 -44
  53. data/lib/fun_ci/animation_renderer.rb +0 -142
  54. data/lib/fun_ci/ansi.rb +0 -32
  55. data/lib/fun_ci/background_wrapper.rb +0 -27
  56. data/lib/fun_ci/board_data.rb +0 -51
  57. data/lib/fun_ci/database.rb +0 -50
  58. data/lib/fun_ci/duration_formatter.rb +0 -23
  59. data/lib/fun_ci/header_animation_manager.rb +0 -69
  60. data/lib/fun_ci/header_animation_player.rb +0 -43
  61. data/lib/fun_ci/hook_writer.rb +0 -73
  62. data/lib/fun_ci/installer.rb +0 -53
  63. data/lib/fun_ci/looping_animation_player.rb +0 -43
  64. data/lib/fun_ci/maven_linter_detector.rb +0 -24
  65. data/lib/fun_ci/pipeline_forker.rb +0 -36
  66. data/lib/fun_ci/pipeline_recorder.rb +0 -72
  67. data/lib/fun_ci/pipeline_run.rb +0 -57
  68. data/lib/fun_ci/progress_reporter.rb +0 -29
  69. data/lib/fun_ci/project_config.rb +0 -40
  70. data/lib/fun_ci/project_detector.rb +0 -18
  71. data/lib/fun_ci/relative_time.rb +0 -20
  72. data/lib/fun_ci/row_formatter.rb +0 -106
  73. data/lib/fun_ci/screen.rb +0 -95
  74. data/lib/fun_ci/setup_checker.rb +0 -28
  75. data/lib/fun_ci/spinner.rb +0 -22
  76. data/lib/fun_ci/stage_change_detector.rb +0 -56
  77. data/lib/fun_ci/stage_job.rb +0 -44
  78. data/lib/fun_ci/stage_runner.rb +0 -108
  79. data/lib/fun_ci/stale_pipeline_canceller.rb +0 -51
  80. data/lib/fun_ci/streak_counter.rb +0 -30
  81. data/lib/fun_ci/template_writer.rb +0 -51
  82. 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.1.0"
10
+ VERSION = "1.2.0"
11
11
 
12
12
  BUILD_TIMEOUT = 30
13
13
  FAST_SUITE_TIMEOUT = 10