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
@@ -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
@@ -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,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
@@ -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,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
@@ -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