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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/README.md +3 -1
  4. data/exe/fun-ci-trigger +6 -6
  5. data/exe/fun-ci-tui +9 -4
  6. data/lib/fun_ci/animations/celebrate.rb +210 -0
  7. data/lib/fun_ci/animations/explosion.rb +138 -0
  8. data/lib/fun_ci/animations/flash.rb +205 -0
  9. data/lib/fun_ci/animations/idle.rb +127 -0
  10. data/lib/fun_ci/animations/leprechauns.rb +240 -0
  11. data/lib/fun_ci/animations/running.rb +129 -0
  12. data/lib/fun_ci/animations/success.rb +281 -0
  13. data/lib/fun_ci/animations/yay.rb +169 -0
  14. data/lib/fun_ci/cli.rb +20 -14
  15. data/lib/fun_ci/persistence/database.rb +52 -0
  16. data/lib/fun_ci/persistence/pipeline_recorder.rb +74 -0
  17. data/lib/fun_ci/persistence/pipeline_run.rb +59 -0
  18. data/lib/fun_ci/persistence/stage_job.rb +46 -0
  19. data/lib/fun_ci/{state_machine.rb → persistence/state_machine.rb} +3 -12
  20. data/lib/fun_ci/pipeline/background_wrapper.rb +27 -0
  21. data/lib/fun_ci/pipeline/pipeline_forker.rb +38 -0
  22. data/lib/fun_ci/pipeline/process_runner.rb +28 -0
  23. data/lib/fun_ci/pipeline/progress_reporter.rb +31 -0
  24. data/lib/fun_ci/pipeline/stage_runner.rb +85 -0
  25. data/lib/fun_ci/pipeline/stale_pipeline_canceller.rb +53 -0
  26. data/lib/fun_ci/pipeline/trigger.rb +153 -0
  27. data/lib/fun_ci/setup/hook_writer.rb +75 -0
  28. data/lib/fun_ci/setup/installer.rb +55 -0
  29. data/lib/fun_ci/setup/maven_linter_detector.rb +26 -0
  30. data/lib/fun_ci/setup/project_config.rb +42 -0
  31. data/lib/fun_ci/setup/project_detector.rb +22 -0
  32. data/lib/fun_ci/setup/setup_checker.rb +30 -0
  33. data/lib/fun_ci/setup/template_writer.rb +53 -0
  34. data/lib/fun_ci/tui/admin_tui.rb +90 -0
  35. data/lib/fun_ci/tui/animation.rb +49 -0
  36. data/lib/fun_ci/tui/animation_compositor.rb +107 -0
  37. data/lib/fun_ci/tui/animation_frames.rb +112 -0
  38. data/lib/fun_ci/tui/animation_library.rb +46 -0
  39. data/lib/fun_ci/tui/animation_renderer.rb +144 -0
  40. data/lib/fun_ci/tui/ansi.rb +34 -0
  41. data/lib/fun_ci/tui/board_data.rb +53 -0
  42. data/lib/fun_ci/tui/board_renderer.rb +105 -0
  43. data/lib/fun_ci/tui/duration_formatter.rb +24 -0
  44. data/lib/fun_ci/tui/header_animation_manager.rb +71 -0
  45. data/lib/fun_ci/tui/header_animation_player.rb +45 -0
  46. data/lib/fun_ci/tui/key_handler.rb +86 -0
  47. data/lib/fun_ci/tui/looping_animation_player.rb +45 -0
  48. data/lib/fun_ci/tui/relative_time.rb +22 -0
  49. data/lib/fun_ci/tui/row_formatter.rb +108 -0
  50. data/lib/fun_ci/tui/screen.rb +103 -0
  51. data/lib/fun_ci/tui/spinner.rb +24 -0
  52. data/lib/fun_ci/tui/stage_change_detector.rb +58 -0
  53. data/lib/fun_ci/tui/streak_counter.rb +29 -0
  54. data/lib/fun_ci/tui/terminal_input.rb +69 -0
  55. data/lib/fun_ci.rb +6 -6
  56. metadata +49 -28
  57. data/lib/fun_ci/admin_tui.rb +0 -226
  58. data/lib/fun_ci/ansi.rb +0 -21
  59. data/lib/fun_ci/background_wrapper.rb +0 -27
  60. data/lib/fun_ci/board_data.rb +0 -51
  61. data/lib/fun_ci/database.rb +0 -50
  62. data/lib/fun_ci/duration_formatter.rb +0 -23
  63. data/lib/fun_ci/hook_writer.rb +0 -73
  64. data/lib/fun_ci/installer.rb +0 -53
  65. data/lib/fun_ci/maven_linter_detector.rb +0 -24
  66. data/lib/fun_ci/pipeline_forker.rb +0 -36
  67. data/lib/fun_ci/pipeline_recorder.rb +0 -72
  68. data/lib/fun_ci/pipeline_run.rb +0 -57
  69. data/lib/fun_ci/progress_reporter.rb +0 -29
  70. data/lib/fun_ci/project_config.rb +0 -40
  71. data/lib/fun_ci/project_detector.rb +0 -18
  72. data/lib/fun_ci/relative_time.rb +0 -20
  73. data/lib/fun_ci/row_formatter.rb +0 -106
  74. data/lib/fun_ci/screen.rb +0 -81
  75. data/lib/fun_ci/setup_checker.rb +0 -28
  76. data/lib/fun_ci/spinner.rb +0 -22
  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
@@ -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
@@ -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
@@ -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,81 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "ansi"
4
-
5
- module FunCi
6
- class Screen
7
- def initialize(output: $stdout, width: 80)
8
- @output = output
9
- @width = width
10
- end
11
-
12
- def width=(new_width)
13
- return if new_width == @width
14
-
15
- clear
16
- @width = new_width
17
- end
18
-
19
- def render_header(streak_text:)
20
- title = " fun-ci"
21
- if streak_text
22
- streak_display = Ansi.green(streak_text)
23
- plain_streak = streak_text
24
- padding = @width - title.length - plain_streak.length
25
- padding = 1 if padding < 1
26
- line = "#{title}#{" " * padding}#{streak_display}"
27
- else
28
- line = title.ljust(@width)
29
- end
30
- println Ansi.bg_charcoal(Ansi.white(line))
31
- end
32
-
33
- def render_footer(empty: false, confirming: false)
34
- if confirming
35
- println Ansi.dim(" Cancel running pipeline? y/n")
36
- elsif empty
37
- println Ansi.dim(" q quit")
38
- else
39
- println Ansi.dim(" j/k move c cancel q quit")
40
- end
41
- end
42
-
43
- def render_empty_state
44
- println ""
45
- println ""
46
- println " No runs yet."
47
- println ""
48
- println " Trigger one: fun-ci trigger HEAD"
49
- println " Or hook it: fun-ci install-hook pre-push"
50
- println ""
51
- end
52
-
53
- def render_board(rows, cursor_index:)
54
- rows.each_with_index do |row, i|
55
- if cursor_index == i
56
- # Replace leading spaces with cursor marker
57
- println "> #{row.lstrip}"
58
- else
59
- println row
60
- end
61
- println unless i == rows.length - 1
62
- end
63
- end
64
-
65
- def println(text = "")
66
- @output.print "#{text}\e[K\r\n"
67
- end
68
-
69
- def clear
70
- @output.print "\e[2J\e[H"
71
- end
72
-
73
- def move_cursor_home
74
- @output.print "\e[H"
75
- end
76
-
77
- def clear_below
78
- @output.print "\e[J"
79
- end
80
- end
81
- end
@@ -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
@@ -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,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
@@ -1,108 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "open3"
4
- require "timeout"
5
-
6
- module FunCi
7
- class StageRunner
8
- def initialize(commit_hash:, stdout:, command_runner: nil, time_budgets: {}, recorder: NullRecorder.new)
9
- @commit_hash = commit_hash
10
- @stdout = stdout
11
- @command_runner = command_runner
12
- @time_budgets = time_budgets
13
- @recorder = recorder
14
- end
15
-
16
- def run_stage(config, stage)
17
- script = config.script_path(stage)
18
- budget = @time_budgets[stage]
19
-
20
- job_id = @recorder.start_stage(stage)
21
- output, status, timed_out = run_with_timeout(script, budget)
22
-
23
- if timed_out
24
- @recorder.end_stage(job_id, "timed_out")
25
- @stdout.puts "#{stage_label(stage)} killed -- exceeded #{budget}s time budget."
26
- @stdout.puts budget_advice(stage)
27
- return false
28
- end
29
-
30
- unless status.success?
31
- @recorder.end_stage(job_id, "failed")
32
- @stdout.puts output unless output.empty?
33
- @stdout.puts "#{stage_label(stage)} failed."
34
- return false
35
- end
36
-
37
- @recorder.end_stage(job_id, "completed")
38
- true
39
- end
40
-
41
- private
42
-
43
- def run_with_timeout(script, budget)
44
- cmd = "#{script} #{@commit_hash}"
45
-
46
- if @command_runner
47
- begin
48
- Timeout.timeout(budget) do
49
- output, status = @command_runner.call(cmd)
50
- [output, status, false]
51
- end
52
- rescue Timeout::Error
53
- ["", nil, true]
54
- end
55
- else
56
- run_process_with_timeout(cmd, budget)
57
- end
58
- end
59
-
60
- def run_process_with_timeout(cmd, budget)
61
- pid = nil
62
- output = ""
63
- r, w = IO.pipe
64
- pid = Process.spawn(cmd, out: w, err: w)
65
- w.close
66
-
67
- begin
68
- Timeout.timeout(budget) do
69
- output = r.read
70
- _, status = Process.waitpid2(pid)
71
- pid = nil
72
- [output, status, false]
73
- end
74
- rescue Timeout::Error
75
- Process.kill("TERM", pid) rescue nil
76
- Process.kill("KILL", pid) rescue nil
77
- Process.waitpid(pid) rescue nil
78
- r.close rescue nil
79
- ["", nil, true]
80
- ensure
81
- r.close rescue nil
82
- end
83
- end
84
-
85
- def stage_label(stage)
86
- case stage
87
- when "lint" then "Lint"
88
- when "build" then "Build"
89
- when "fast" then "Fast suite"
90
- when "slow" then "Slow suite"
91
- else stage
92
- end
93
- end
94
-
95
- def budget_advice(stage)
96
- case stage
97
- when "lint"
98
- "Trim your linter config or split into stages."
99
- when "build"
100
- "Keep your build efficient and not let it become a bottleneck."
101
- when "fast"
102
- "Your fast tests have gotten too slow. Split or speed them up."
103
- when "slow"
104
- "Pare down integration tests, parallelise, or raise the budget."
105
- end
106
- end
107
- end
108
- end
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "pipeline_run"
4
-
5
- module FunCi
6
- class StalePipelineCanceller
7
- def initialize(db:, branch:, stdout:, process_killer: nil)
8
- @db = db
9
- @branch = branch
10
- @stdout = stdout
11
- @process_killer = process_killer || method(:default_process_killer)
12
- end
13
-
14
- def cancel(new_commit_hash:)
15
- run = PipelineRun.find_running_with_pid(@db, @branch)
16
- return unless run
17
-
18
- kill_process(run[:pid])
19
- PipelineRun.update_status(@db, run[:id], "cancelled")
20
- @stdout.puts "Cancelled stale pipeline for #{run[:commit_hash]}. Starting fresh for #{new_commit_hash}."
21
- end
22
-
23
- def store_pid(run_id, pid)
24
- PipelineRun.store_pid(@db, run_id, pid)
25
- end
26
-
27
- private
28
-
29
- def kill_process(pid)
30
- @process_killer.call(0, pid)
31
- safe_signal("TERM", pid)
32
- safe_signal("KILL", pid)
33
- begin
34
- Process.waitpid(pid)
35
- rescue Errno::ECHILD, Errno::ESRCH # rubocop:disable Lint/SuppressedException
36
- end
37
- rescue Errno::ESRCH
38
- # Process already dead
39
- end
40
-
41
- def safe_signal(signal, pid)
42
- @process_killer.call(signal, pid)
43
- rescue Errno::ESRCH
44
- # Already dead
45
- end
46
-
47
- def default_process_killer(signal, pid)
48
- Process.kill(signal, pid)
49
- end
50
- end
51
- end
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FunCi
4
- module StreakCounter
5
- PASS_STATUS = "completed"
6
- ACTIVE_STATUSES = %w[running scheduled].freeze
7
-
8
- # Counts consecutive passed runs from the most recent terminal run.
9
- # Skips running/scheduled pipelines.
10
- # Returns nil if no terminal runs exist, 0 if streak is broken.
11
- def self.count(runs)
12
- terminal_runs = runs.reject { |r| ACTIVE_STATUSES.include?(r[:status]) }
13
- return nil if terminal_runs.empty?
14
-
15
- streak = 0
16
- terminal_runs.each do |run|
17
- break unless run[:status] == PASS_STATUS
18
- streak += 1
19
- end
20
- streak
21
- end
22
-
23
- def self.format_text(streak)
24
- return nil if streak.nil?
25
- return "Streak broken" if streak == 0
26
-
27
- "#{streak} in a row!"
28
- end
29
- end
30
- end
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FunCi
4
- class TemplateWriter
5
- TEMPLATES = {
6
- ruby_bundler: {
7
- "lint.sh" => "#!/bin/sh\nbundle exec rubocop\n",
8
- "build.sh" => "#!/bin/sh\nbundle install --quiet\n",
9
- "fast.sh" => "#!/bin/sh\nbundle exec rake test\n",
10
- "slow.sh" => "#!/bin/sh\nbundle exec rake test:slow\n"
11
- },
12
- jvm_gradle_kotlin: {
13
- "lint.sh" => "#!/bin/sh\n./gradlew check -x test\n",
14
- "build.sh" => "#!/bin/sh\n./gradlew assemble\n",
15
- "fast.sh" => "#!/bin/sh\n./gradlew test\n",
16
- "slow.sh" => "#!/bin/sh\n./gradlew integrationTest\n"
17
- },
18
- jvm_gradle_groovy: {
19
- "lint.sh" => "#!/bin/sh\n./gradlew check -x test\n",
20
- "build.sh" => "#!/bin/sh\n./gradlew assemble\n",
21
- "fast.sh" => "#!/bin/sh\n./gradlew test\n",
22
- "slow.sh" => "#!/bin/sh\n./gradlew integrationTest\n"
23
- },
24
- jvm_maven: {
25
- "lint.sh" => "#!/bin/sh\nmvn verify -DskipTests\n",
26
- "build.sh" => "#!/bin/sh\nmvn compile\n",
27
- "fast.sh" => "#!/bin/sh\nmvn test\n",
28
- "slow.sh" => "#!/bin/sh\nmvn verify\n"
29
- }
30
- }.freeze
31
-
32
- def initialize(template_id, target_dir, lint_override: nil)
33
- @template_id = template_id
34
- @target_dir = target_dir
35
- @lint_override = lint_override
36
- end
37
-
38
- def write
39
- scripts = TEMPLATES.fetch(@template_id)
40
- fun_ci_dir = File.join(@target_dir, ".fun-ci")
41
- Dir.mkdir(fun_ci_dir)
42
-
43
- scripts.each do |name, content|
44
- content = "#!/bin/sh\n#{@lint_override}\n" if name == "lint.sh" && @lint_override
45
- path = File.join(fun_ci_dir, name)
46
- File.write(path, content)
47
- File.chmod(0o755, path)
48
- end
49
- end
50
- end
51
- end