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
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../persistence/pipeline_run"
|
|
4
|
+
|
|
5
|
+
module FunCi
|
|
6
|
+
module Pipeline
|
|
7
|
+
class StalePipelineCanceller
|
|
8
|
+
def initialize(db:, branch:, stdout:, process_killer: nil)
|
|
9
|
+
@db = db
|
|
10
|
+
@branch = branch
|
|
11
|
+
@stdout = stdout
|
|
12
|
+
@process_killer = process_killer || method(:default_process_killer)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def cancel(new_commit_hash:)
|
|
16
|
+
run = Persistence::PipelineRun.find_running_with_pid(@db, @branch)
|
|
17
|
+
return unless run
|
|
18
|
+
|
|
19
|
+
kill_process(run[:pid])
|
|
20
|
+
Persistence::PipelineRun.update_status(@db, run[:id], "cancelled")
|
|
21
|
+
@stdout.puts "Cancelled stale pipeline for #{run[:commit_hash]}. Starting fresh for #{new_commit_hash}."
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def store_pid(run_id, pid)
|
|
25
|
+
Persistence::PipelineRun.store_pid(@db, run_id, pid)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def kill_process(pid)
|
|
31
|
+
@process_killer.call(0, pid)
|
|
32
|
+
safe_signal("TERM", pid)
|
|
33
|
+
safe_signal("KILL", pid)
|
|
34
|
+
begin
|
|
35
|
+
Process.waitpid(pid)
|
|
36
|
+
rescue Errno::ECHILD, Errno::ESRCH # rubocop:disable Lint/SuppressedException
|
|
37
|
+
end
|
|
38
|
+
rescue Errno::ESRCH
|
|
39
|
+
# Process already dead
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def safe_signal(signal, pid)
|
|
43
|
+
@process_killer.call(signal, pid)
|
|
44
|
+
rescue Errno::ESRCH
|
|
45
|
+
# Already dead
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def default_process_killer(signal, pid)
|
|
49
|
+
Process.kill(signal, pid)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require_relative "../setup/project_config"
|
|
5
|
+
require_relative "../persistence/pipeline_recorder"
|
|
6
|
+
require_relative "background_wrapper"
|
|
7
|
+
require_relative "stage_runner"
|
|
8
|
+
require_relative "stale_pipeline_canceller"
|
|
9
|
+
require_relative "progress_reporter"
|
|
10
|
+
require_relative "pipeline_forker"
|
|
11
|
+
require_relative "process_runner"
|
|
12
|
+
|
|
13
|
+
module FunCi
|
|
14
|
+
module Pipeline
|
|
15
|
+
class Trigger
|
|
16
|
+
include ProcessRunner
|
|
17
|
+
NULL_SHA = ("0" * 40).freeze
|
|
18
|
+
DEFAULT_BUDGETS = { "lint" => 30, "build" => 30, "fast" => 10, "slow" => 300 }.freeze
|
|
19
|
+
|
|
20
|
+
def self.run_from_args(args, stdout: $stdout, stderr: $stderr, recorder: Persistence::NullRecorder.new, pipeline_forker: nil)
|
|
21
|
+
positional = args.reject { |a| a.start_with?("--") }
|
|
22
|
+
if positional.length < 2
|
|
23
|
+
stderr.puts "fun-ci: commit hash and branch name are required."
|
|
24
|
+
stderr.puts "Usage: fun-ci trigger <commit-hash> <branch>"
|
|
25
|
+
return 1
|
|
26
|
+
end
|
|
27
|
+
commit_hash, branch = positional
|
|
28
|
+
if args.include?("--no-validate")
|
|
29
|
+
db_path = recorder.db_path
|
|
30
|
+
recorder.close
|
|
31
|
+
(pipeline_forker || PipelineForker.method(:fork_pipeline)).call(
|
|
32
|
+
commit_hash: commit_hash, branch: branch, db_path: db_path
|
|
33
|
+
)
|
|
34
|
+
return 0
|
|
35
|
+
end
|
|
36
|
+
new(project_root: Dir.pwd, commit_hash: commit_hash, branch: branch,
|
|
37
|
+
stdout: stdout, stderr: stderr, recorder: recorder).run
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
attr_writer :command_runner
|
|
41
|
+
|
|
42
|
+
def initialize(project_root:, commit_hash:, branch:, stdout: $stdout, stderr: $stderr, command_runner: nil, time_budgets: {}, commit_validator: nil, recorder: Persistence::NullRecorder.new, background_launcher: nil)
|
|
43
|
+
@project_root = project_root
|
|
44
|
+
@commit_hash = commit_hash
|
|
45
|
+
@branch = branch
|
|
46
|
+
@stdout = stdout
|
|
47
|
+
@stderr = stderr
|
|
48
|
+
@command_runner = command_runner
|
|
49
|
+
@time_budgets = DEFAULT_BUDGETS.merge(time_budgets)
|
|
50
|
+
@commit_validator = commit_validator || method(:default_commit_validator)
|
|
51
|
+
@recorder = recorder
|
|
52
|
+
@background_launcher = background_launcher || method(:default_background_launcher)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def run
|
|
56
|
+
config = Setup::ProjectConfig.new(@project_root)
|
|
57
|
+
return handle_config_errors(config) if config.validate.any?
|
|
58
|
+
unless @commit_hash == NULL_SHA || @commit_validator.call(@commit_hash)
|
|
59
|
+
@stderr.puts "fun-ci: commit #{@commit_hash} not found in this repository."
|
|
60
|
+
return 1
|
|
61
|
+
end
|
|
62
|
+
cancel_stale_pipelines
|
|
63
|
+
@recorder.create_run(commit_hash: @commit_hash, branch: @branch, project_path: @project_root)
|
|
64
|
+
stage_runner = make_stage_runner
|
|
65
|
+
progress = ProgressReporter.new(stdout: @stdout)
|
|
66
|
+
results = run_phase_one(stage_runner, config)
|
|
67
|
+
progress.phase_one_result(results)
|
|
68
|
+
unless results.values.all?
|
|
69
|
+
@recorder.fail_run
|
|
70
|
+
return 1
|
|
71
|
+
end
|
|
72
|
+
spawn_slow_suite(config)
|
|
73
|
+
progress.slow_launched
|
|
74
|
+
fast_runner = make_stage_runner
|
|
75
|
+
fast_passed = fast_runner.run_stage(config, "fast")
|
|
76
|
+
progress.fast_result(fast_passed)
|
|
77
|
+
unless fast_passed
|
|
78
|
+
@recorder.fail_run
|
|
79
|
+
return 1
|
|
80
|
+
end
|
|
81
|
+
0
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def handle_config_errors(config)
|
|
87
|
+
config.validate.each { |e| @stdout.puts "fun-ci: #{e}" }
|
|
88
|
+
unless config.folder_exists?
|
|
89
|
+
@stdout.puts "Create .fun-ci/lint.sh, build.sh, fast.sh, and slow.sh to set up this project."
|
|
90
|
+
@stdout.puts "Commit will proceed without CI."
|
|
91
|
+
end
|
|
92
|
+
0
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def run_phase_one(stage_runner, config)
|
|
96
|
+
results = {}
|
|
97
|
+
threads = %w[lint build].map do |stage|
|
|
98
|
+
Thread.new { results[stage] = stage_runner.run_stage(config, stage) }
|
|
99
|
+
end
|
|
100
|
+
threads.each(&:join)
|
|
101
|
+
results
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def spawn_slow_suite(config)
|
|
105
|
+
cmd = "#{config.script_path("slow")} #{@commit_hash}"
|
|
106
|
+
job_id = @recorder.start_stage("slow")
|
|
107
|
+
budget = @time_budgets["slow"]
|
|
108
|
+
executor = if @command_runner
|
|
109
|
+
runner = @command_runner
|
|
110
|
+
-> do
|
|
111
|
+
output, status = runner.call(cmd)
|
|
112
|
+
[output, status, false]
|
|
113
|
+
rescue Timeout::Error
|
|
114
|
+
["", nil, true]
|
|
115
|
+
end
|
|
116
|
+
else
|
|
117
|
+
-> { run_process_with_timeout(cmd, budget) }
|
|
118
|
+
end
|
|
119
|
+
@background_launcher.call(
|
|
120
|
+
db_path: @recorder.db_path, pipeline_run_id: @recorder.pipeline_run_id,
|
|
121
|
+
job_id: job_id, executor: executor
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def make_stage_runner
|
|
126
|
+
StageRunner.new(commit_hash: @commit_hash, stdout: @stdout,
|
|
127
|
+
command_runner: @command_runner, time_budgets: @time_budgets, recorder: @recorder)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def cancel_stale_pipelines
|
|
131
|
+
return unless @recorder.db
|
|
132
|
+
StalePipelineCanceller.new(db: @recorder.db, branch: @branch, stdout: @stdout)
|
|
133
|
+
.cancel(new_commit_hash: @commit_hash)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def default_background_launcher(db_path:, pipeline_run_id:, job_id:, executor:)
|
|
137
|
+
@recorder.close
|
|
138
|
+
pid = fork do
|
|
139
|
+
recorder = Persistence::DbRecorder.for_background(db_path, pipeline_run_id)
|
|
140
|
+
BackgroundWrapper.new(recorder: recorder, job_id: job_id, executor: executor).run
|
|
141
|
+
recorder.close
|
|
142
|
+
end
|
|
143
|
+
@recorder = Persistence::DbRecorder.for_background(db_path, pipeline_run_id)
|
|
144
|
+
Process.detach(pid)
|
|
145
|
+
Persistence::PipelineRun.store_pid(@recorder.db, pipeline_run_id, pid)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def default_commit_validator(hash)
|
|
149
|
+
Open3.capture2e("git", "cat-file", "-t", hash).last.success?
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module FunCi
|
|
6
|
+
module Setup
|
|
7
|
+
class HookWriter
|
|
8
|
+
ALLOWED_HOOKS = %w[pre-commit pre-push].freeze
|
|
9
|
+
MARKER = "# fun-ci-managed-hook"
|
|
10
|
+
|
|
11
|
+
HOOK_COMMANDS = {
|
|
12
|
+
"pre-commit" => "fun-ci trigger --no-validate",
|
|
13
|
+
"pre-push" => "fun-ci trigger"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
HOOK_TEMPLATE = <<~SH
|
|
17
|
+
#!/bin/sh
|
|
18
|
+
#{MARKER}
|
|
19
|
+
COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "0000000000000000000000000000000000000000")
|
|
20
|
+
BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
|
21
|
+
%<command>s "$COMMIT" "$BRANCH"
|
|
22
|
+
SH
|
|
23
|
+
|
|
24
|
+
def self.run(project_root:, hook_type:, stdout: $stdout)
|
|
25
|
+
new(project_root: project_root, hook_type: hook_type, stdout: stdout).run
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(project_root:, hook_type:, stdout:)
|
|
29
|
+
@project_root = project_root
|
|
30
|
+
@hook_type = hook_type
|
|
31
|
+
@stdout = stdout
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def run
|
|
35
|
+
return reject("Not a git repository — no .git/ found.") unless git_repo?
|
|
36
|
+
return reject("Unknown hook type: #{@hook_type}") unless ALLOWED_HOOKS.include?(@hook_type)
|
|
37
|
+
return skip("Hook #{@hook_type} already exists from another tool — skipping.") if foreign_hook?
|
|
38
|
+
|
|
39
|
+
write_hook
|
|
40
|
+
@stdout.puts "Installed #{@hook_type} hook."
|
|
41
|
+
0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def git_repo?
|
|
47
|
+
Dir.exist?(File.join(@project_root, ".git"))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def hook_path
|
|
51
|
+
File.join(@project_root, ".git", "hooks", @hook_type)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def foreign_hook?
|
|
55
|
+
File.exist?(hook_path) && !File.read(hook_path).include?(MARKER)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def write_hook
|
|
59
|
+
FileUtils.mkdir_p(File.join(@project_root, ".git", "hooks"))
|
|
60
|
+
File.write(hook_path, format(HOOK_TEMPLATE, command: HOOK_COMMANDS[@hook_type]))
|
|
61
|
+
File.chmod(0o755, hook_path)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def reject(message)
|
|
65
|
+
@stdout.puts message
|
|
66
|
+
1
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def skip(message)
|
|
70
|
+
@stdout.puts message
|
|
71
|
+
0
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
module Setup
|
|
9
|
+
class Installer
|
|
10
|
+
def self.run(project_root:, stdout: $stdout, pom_reader: nil)
|
|
11
|
+
new(project_root: project_root, stdout: stdout, pom_reader: pom_reader).run
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(project_root:, stdout:, pom_reader: nil)
|
|
15
|
+
@project_root = project_root
|
|
16
|
+
@stdout = stdout
|
|
17
|
+
@pom_reader = pom_reader || ->(path) { File.read(path) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run
|
|
21
|
+
if Dir.exist?(File.join(@project_root, ".fun-ci"))
|
|
22
|
+
@stdout.puts ".fun-ci/ already exists — skipping init."
|
|
23
|
+
return 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
filenames = Dir.children(@project_root)
|
|
27
|
+
detected = ProjectDetector.new(filenames).detect
|
|
28
|
+
|
|
29
|
+
if detected == :unknown
|
|
30
|
+
@stdout.puts "Could not detect project type. Create .fun-ci/ manually."
|
|
31
|
+
return 1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@stdout.puts "Detected: #{detected.to_s.tr("_", " ")}"
|
|
35
|
+
|
|
36
|
+
lint_override = detect_maven_linter(detected)
|
|
37
|
+
TemplateWriter.new(detected, @project_root, lint_override: lint_override).write
|
|
38
|
+
|
|
39
|
+
@stdout.puts "Created .fun-ci/ with template scripts."
|
|
40
|
+
0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def detect_maven_linter(detected)
|
|
46
|
+
return nil unless detected == :jvm_maven
|
|
47
|
+
|
|
48
|
+
pom_path = File.join(@project_root, "pom.xml")
|
|
49
|
+
pom_content = @pom_reader.call(pom_path)
|
|
50
|
+
command = MavenLinterDetector.new(pom_content).lint_command
|
|
51
|
+
command == MavenLinterDetector::DEFAULT_COMMAND ? nil : command
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Setup
|
|
5
|
+
class MavenLinterDetector
|
|
6
|
+
LINTERS = [
|
|
7
|
+
{ artifact_id: "detekt-maven-plugin", command: "mvn detekt:check" },
|
|
8
|
+
{ artifact_id: "ktlint-maven-plugin", command: "mvn ktlint:check" },
|
|
9
|
+
{ artifact_id: "maven-checkstyle-plugin", command: "mvn checkstyle:check" },
|
|
10
|
+
{ artifact_id: "spotbugs-maven-plugin", command: "mvn spotbugs:check" },
|
|
11
|
+
{ artifact_id: "maven-pmd-plugin", command: "mvn pmd:check" }
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
DEFAULT_COMMAND = "mvn verify -DskipTests"
|
|
15
|
+
|
|
16
|
+
def initialize(pom_content)
|
|
17
|
+
@pom_content = pom_content
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def lint_command
|
|
21
|
+
match = LINTERS.find { |linter| @pom_content.include?(linter[:artifact_id]) }
|
|
22
|
+
match ? match[:command] : DEFAULT_COMMAND
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Setup
|
|
5
|
+
class ProjectConfig
|
|
6
|
+
REQUIRED_SCRIPTS = %w[lint.sh build.sh fast.sh slow.sh].freeze
|
|
7
|
+
|
|
8
|
+
def initialize(project_root)
|
|
9
|
+
@project_root = project_root
|
|
10
|
+
@fun_ci_dir = File.join(project_root, ".fun-ci")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def folder_exists?
|
|
14
|
+
Dir.exist?(@fun_ci_dir)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def validate
|
|
18
|
+
errors = []
|
|
19
|
+
|
|
20
|
+
unless folder_exists?
|
|
21
|
+
errors << "No .fun-ci/ folder found in #{@project_root}"
|
|
22
|
+
return errors
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
REQUIRED_SCRIPTS.each do |script|
|
|
26
|
+
path = File.join(@fun_ci_dir, script)
|
|
27
|
+
if !File.exist?(path)
|
|
28
|
+
errors << ".fun-ci/#{script} is not found"
|
|
29
|
+
elsif !File.executable?(path)
|
|
30
|
+
errors << ".fun-ci/#{script} is not executable"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
errors
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def script_path(stage)
|
|
38
|
+
File.join(@fun_ci_dir, "#{stage}.sh")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Setup
|
|
5
|
+
class ProjectDetector
|
|
6
|
+
def initialize(filenames)
|
|
7
|
+
@filenames = filenames
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def detect
|
|
11
|
+
return :ruby_bundler if @filenames.include?("Gemfile")
|
|
12
|
+
return :jvm_gradle_kotlin if @filenames.include?("build.gradle.kts")
|
|
13
|
+
return :jvm_gradle_kotlin if @filenames.include?("settings.gradle.kts")
|
|
14
|
+
return :jvm_gradle_groovy if @filenames.include?("build.gradle")
|
|
15
|
+
return :jvm_gradle_groovy if @filenames.include?("settings.gradle")
|
|
16
|
+
return :jvm_maven if @filenames.include?("pom.xml")
|
|
17
|
+
|
|
18
|
+
:unknown
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "project_config"
|
|
4
|
+
|
|
5
|
+
module FunCi
|
|
6
|
+
module Setup
|
|
7
|
+
class SetupChecker
|
|
8
|
+
def self.run(project_root:, stdout: $stdout, stderr: $stderr)
|
|
9
|
+
new(project_root: project_root, stdout: stdout).run
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(project_root:, stdout:)
|
|
13
|
+
@config = ProjectConfig.new(project_root)
|
|
14
|
+
@stdout = stdout
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
errors = @config.validate
|
|
19
|
+
|
|
20
|
+
if errors.empty?
|
|
21
|
+
@stdout.puts "All OK — project is configured."
|
|
22
|
+
0
|
|
23
|
+
else
|
|
24
|
+
errors.each { |e| @stdout.puts e }
|
|
25
|
+
1
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Setup
|
|
5
|
+
class TemplateWriter
|
|
6
|
+
TEMPLATES = {
|
|
7
|
+
ruby_bundler: {
|
|
8
|
+
"lint.sh" => "#!/bin/sh\nbundle exec rubocop\n",
|
|
9
|
+
"build.sh" => "#!/bin/sh\nbundle install --quiet\n",
|
|
10
|
+
"fast.sh" => "#!/bin/sh\nbundle exec rake test\n",
|
|
11
|
+
"slow.sh" => "#!/bin/sh\nbundle exec rake test:slow\n"
|
|
12
|
+
},
|
|
13
|
+
jvm_gradle_kotlin: {
|
|
14
|
+
"lint.sh" => "#!/bin/sh\n./gradlew check -x test\n",
|
|
15
|
+
"build.sh" => "#!/bin/sh\n./gradlew assemble\n",
|
|
16
|
+
"fast.sh" => "#!/bin/sh\n./gradlew test\n",
|
|
17
|
+
"slow.sh" => "#!/bin/sh\n./gradlew integrationTest\n"
|
|
18
|
+
},
|
|
19
|
+
jvm_gradle_groovy: {
|
|
20
|
+
"lint.sh" => "#!/bin/sh\n./gradlew check -x test\n",
|
|
21
|
+
"build.sh" => "#!/bin/sh\n./gradlew assemble\n",
|
|
22
|
+
"fast.sh" => "#!/bin/sh\n./gradlew test\n",
|
|
23
|
+
"slow.sh" => "#!/bin/sh\n./gradlew integrationTest\n"
|
|
24
|
+
},
|
|
25
|
+
jvm_maven: {
|
|
26
|
+
"lint.sh" => "#!/bin/sh\nmvn verify -DskipTests\n",
|
|
27
|
+
"build.sh" => "#!/bin/sh\nmvn compile\n",
|
|
28
|
+
"fast.sh" => "#!/bin/sh\nmvn test\n",
|
|
29
|
+
"slow.sh" => "#!/bin/sh\nmvn verify\n"
|
|
30
|
+
}
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
def initialize(template_id, target_dir, lint_override: nil)
|
|
34
|
+
@template_id = template_id
|
|
35
|
+
@target_dir = target_dir
|
|
36
|
+
@lint_override = lint_override
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def write
|
|
40
|
+
scripts = TEMPLATES.fetch(@template_id)
|
|
41
|
+
fun_ci_dir = File.join(@target_dir, ".fun-ci")
|
|
42
|
+
Dir.mkdir(fun_ci_dir)
|
|
43
|
+
|
|
44
|
+
scripts.each do |name, content|
|
|
45
|
+
content = "#!/bin/sh\n#{@lint_override}\n" if name == "lint.sh" && @lint_override
|
|
46
|
+
path = File.join(fun_ci_dir, name)
|
|
47
|
+
File.write(path, content)
|
|
48
|
+
File.chmod(0o755, path)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "board_data"
|
|
4
|
+
require_relative "board_renderer"
|
|
5
|
+
require_relative "key_handler"
|
|
6
|
+
require_relative "screen"
|
|
7
|
+
require_relative "spinner"
|
|
8
|
+
require_relative "terminal_input"
|
|
9
|
+
require "io/console"
|
|
10
|
+
|
|
11
|
+
module FunCi
|
|
12
|
+
module Tui
|
|
13
|
+
class AdminTui
|
|
14
|
+
FAST_REFRESH = 0.1
|
|
15
|
+
SLOW_REFRESH = 5.0
|
|
16
|
+
|
|
17
|
+
def initialize(db:, output: $stdout, input: $stdin, width: 80,
|
|
18
|
+
width_provider: nil, height_provider: nil,
|
|
19
|
+
page_size: nil, animation_renderer: nil)
|
|
20
|
+
@board_data = BoardData.new(db, page_size: page_size)
|
|
21
|
+
@terminal_input = TerminalInput.new(input: input, width_provider: width_provider)
|
|
22
|
+
@key_handler = KeyHandler.new(board_data: @board_data)
|
|
23
|
+
@renderer = BoardRenderer.new(
|
|
24
|
+
screen: Screen.new(output: output, width: width),
|
|
25
|
+
spinner: Spinner.new,
|
|
26
|
+
animation_renderer: animation_renderer,
|
|
27
|
+
height_provider: height_provider
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def render_once
|
|
32
|
+
update_width_from_provider
|
|
33
|
+
@renderer.render(
|
|
34
|
+
runs: @board_data.runs,
|
|
35
|
+
streak: @board_data.streak,
|
|
36
|
+
cursor_index: @key_handler.cursor_index,
|
|
37
|
+
confirming: @key_handler.confirming?
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def run
|
|
42
|
+
@renderer.clear
|
|
43
|
+
|
|
44
|
+
begin
|
|
45
|
+
@terminal_input.setup_raw_mode
|
|
46
|
+
@terminal_input.setup_signal_trap
|
|
47
|
+
loop do
|
|
48
|
+
@renderer.begin_frame
|
|
49
|
+
render_once
|
|
50
|
+
key = @terminal_input.read_key_with_timeout(refresh_interval)
|
|
51
|
+
break if key && @key_handler.handle_key(key) == :quit
|
|
52
|
+
end
|
|
53
|
+
ensure
|
|
54
|
+
@terminal_input.restore_terminal
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def resize(new_width)
|
|
59
|
+
@renderer.resize(new_width)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def confirming?
|
|
63
|
+
@key_handler.confirming?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def confirmation_run
|
|
67
|
+
@key_handler.confirmation_run
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def handle_key(key)
|
|
71
|
+
@key_handler.handle_key(key)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def refresh_interval
|
|
77
|
+
return FAST_REFRESH if @renderer.animation_renderer?
|
|
78
|
+
|
|
79
|
+
runs = @board_data.runs
|
|
80
|
+
any_running = runs.any? { |r| r[:status] == "running" }
|
|
81
|
+
any_running ? FAST_REFRESH : SLOW_REFRESH
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def update_width_from_provider
|
|
85
|
+
new_width = @terminal_input.check_width
|
|
86
|
+
@renderer.resize(new_width) if new_width
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Tui
|
|
5
|
+
class Animation
|
|
6
|
+
TYPES = {
|
|
7
|
+
failure: { total_frames: 40, priority: 3 },
|
|
8
|
+
timeout: { total_frames: 4, priority: 2 },
|
|
9
|
+
success: { total_frames: 40, priority: 1 },
|
|
10
|
+
stage_pass: { total_frames: 3, priority: 0 }
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :type, :run_id, :stage, :frame
|
|
14
|
+
|
|
15
|
+
def initialize(type:, run_id:, stage: nil)
|
|
16
|
+
raise ArgumentError, "Unknown animation type: #{type}" unless TYPES.key?(type)
|
|
17
|
+
|
|
18
|
+
@type = type
|
|
19
|
+
@run_id = run_id
|
|
20
|
+
@stage = stage
|
|
21
|
+
@frame = 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def advance!
|
|
25
|
+
@frame += 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def finished?
|
|
29
|
+
frame >= total_frames
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def total_frames
|
|
33
|
+
TYPES[@type][:total_frames]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def priority
|
|
37
|
+
TYPES[@type][:priority]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def has_header?
|
|
41
|
+
%i[failure timeout success].include?(@type)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def has_footer?
|
|
45
|
+
%i[failure success].include?(@type)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|