fun_ci 1.2.1 → 2.0.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 (228) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +218 -0
  3. data/README.md +180 -59
  4. data/lib/fun_ci/agent/age.rb +15 -0
  5. data/lib/fun_ci/agent/command_support.rb +27 -0
  6. data/lib/fun_ci/agent/commands.rb +27 -0
  7. data/lib/fun_ci/agent/context.rb +10 -0
  8. data/lib/fun_ci/agent/digest.rb +49 -0
  9. data/lib/fun_ci/agent/events.rb +47 -0
  10. data/lib/fun_ci/agent/events_command.rb +61 -0
  11. data/lib/fun_ci/agent/evidence_text.rb +46 -0
  12. data/lib/fun_ci/agent/exit_code.rb +11 -0
  13. data/lib/fun_ci/agent/git.rb +31 -0
  14. data/lib/fun_ci/agent/live_pipeline.rb +35 -0
  15. data/lib/fun_ci/agent/options.rb +75 -0
  16. data/lib/fun_ci/agent/output.rb +81 -0
  17. data/lib/fun_ci/agent/report_stage.rb +73 -0
  18. data/lib/fun_ci/agent/reports.rb +47 -0
  19. data/lib/fun_ci/agent/run_report.rb +30 -0
  20. data/lib/fun_ci/agent/runs_command.rb +34 -0
  21. data/lib/fun_ci/agent/runs_text.rb +27 -0
  22. data/lib/fun_ci/agent/snapshots.rb +39 -0
  23. data/lib/fun_ci/agent/stage_summary.rb +30 -0
  24. data/lib/fun_ci/agent/status_command.rb +36 -0
  25. data/lib/fun_ci/agent/status_json.rb +27 -0
  26. data/lib/fun_ci/agent/status_text.rb +37 -0
  27. data/lib/fun_ci/agent/system_clock.rb +11 -0
  28. data/lib/fun_ci/agent/verdict.rb +32 -0
  29. data/lib/fun_ci/agent/wait_command.rb +71 -0
  30. data/lib/fun_ci/agent/waiting.rb +32 -0
  31. data/lib/fun_ci/agent/why_command.rb +50 -0
  32. data/lib/fun_ci/agent/why_json.rb +35 -0
  33. data/lib/fun_ci/agent/why_text.rb +43 -0
  34. data/lib/fun_ci/cli.rb +72 -94
  35. data/lib/fun_ci/cli_help.rb +65 -0
  36. data/lib/fun_ci/console/board_data.rb +66 -0
  37. data/lib/fun_ci/console/board_heartbeat.rb +40 -0
  38. data/lib/fun_ci/console/console_events.rb +20 -0
  39. data/lib/fun_ci/console/console_log.rb +26 -0
  40. data/lib/fun_ci/console/console_loop.rb +41 -0
  41. data/lib/fun_ci/console/console_session.rb +82 -0
  42. data/lib/fun_ci/console/console_state.rb +42 -0
  43. data/lib/fun_ci/console/key_handler.rb +73 -0
  44. data/lib/fun_ci/console/launcher.rb +43 -0
  45. data/lib/fun_ci/console/milestone_events.rb +38 -0
  46. data/lib/fun_ci/console/milestones.rb +25 -0
  47. data/lib/fun_ci/console/renderer_lookup.rb +51 -0
  48. data/lib/fun_ci/console/renderer_process.rb +60 -0
  49. data/lib/fun_ci/console/run_message.rb +33 -0
  50. data/lib/fun_ci/console/stage_change_detector.rb +32 -0
  51. data/lib/fun_ci/console/stage_events.rb +31 -0
  52. data/lib/fun_ci/console/stop_report.rb +39 -0
  53. data/lib/fun_ci/console/streak_counter.rb +19 -0
  54. data/lib/fun_ci/console/view.rb +47 -0
  55. data/lib/fun_ci/evidence/about.rb +12 -0
  56. data/lib/fun_ci/evidence/about_stage.rb +29 -0
  57. data/lib/fun_ci/evidence/caps.rb +77 -0
  58. data/lib/fun_ci/evidence/catalog.rb +83 -0
  59. data/lib/fun_ci/evidence/collector.rb +95 -0
  60. data/lib/fun_ci/evidence/command_output.rb +45 -0
  61. data/lib/fun_ci/evidence/command_reading.rb +38 -0
  62. data/lib/fun_ci/evidence/command_runner.rb +75 -0
  63. data/lib/fun_ci/evidence/context.rb +21 -0
  64. data/lib/fun_ci/evidence/context_document.rb +29 -0
  65. data/lib/fun_ci/evidence/deadline.rb +15 -0
  66. data/lib/fun_ci/evidence/detection.rb +82 -0
  67. data/lib/fun_ci/evidence/document.rb +59 -0
  68. data/lib/fun_ci/evidence/extract_command.rb +83 -0
  69. data/lib/fun_ci/evidence/extract_options.rb +45 -0
  70. data/lib/fun_ci/evidence/extraction.rb +54 -0
  71. data/lib/fun_ci/evidence/extractors/command.rb +52 -0
  72. data/lib/fun_ci/evidence/extractors/grep.rb +49 -0
  73. data/lib/fun_ci/evidence/extractors/json_log.rb +45 -0
  74. data/lib/fun_ci/evidence/extractors/junit_files.rb +32 -0
  75. data/lib/fun_ci/evidence/extractors/last_lines.rb +25 -0
  76. data/lib/fun_ci/evidence/extractors/log_file.rb +62 -0
  77. data/lib/fun_ci/evidence/extractors/process_tree.rb +51 -0
  78. data/lib/fun_ci/evidence/extractors/section.rb +49 -0
  79. data/lib/fun_ci/evidence/findings.rb +14 -0
  80. data/lib/fun_ci/evidence/line_ranges.rb +21 -0
  81. data/lib/fun_ci/evidence/line_scan.rb +22 -0
  82. data/lib/fun_ci/evidence/log_record.rb +58 -0
  83. data/lib/fun_ci/evidence/masking.rb +55 -0
  84. data/lib/fun_ci/evidence/option_check.rb +53 -0
  85. data/lib/fun_ci/evidence/outcome.rb +15 -0
  86. data/lib/fun_ci/evidence/patterns.rb +22 -0
  87. data/lib/fun_ci/evidence/presets/README.md +7 -0
  88. data/lib/fun_ci/evidence/presets/bun-test.yml +6 -0
  89. data/lib/fun_ci/evidence/presets/cargo-test.yml +6 -0
  90. data/lib/fun_ci/evidence/presets/dart-test.yml +6 -0
  91. data/lib/fun_ci/evidence/presets/deno-test.yml +6 -0
  92. data/lib/fun_ci/evidence/presets/dotnet-test.yml +6 -0
  93. data/lib/fun_ci/evidence/presets/ecs.yml +3 -0
  94. data/lib/fun_ci/evidence/presets/eslint.yml +6 -0
  95. data/lib/fun_ci/evidence/presets/exunit.yml +6 -0
  96. data/lib/fun_ci/evidence/presets/gcc.yml +6 -0
  97. data/lib/fun_ci/evidence/presets/go-build.yml +5 -0
  98. data/lib/fun_ci/evidence/presets/go-test.yml +6 -0
  99. data/lib/fun_ci/evidence/presets/gradle.yml +6 -0
  100. data/lib/fun_ci/evidence/presets/gtest.yml +6 -0
  101. data/lib/fun_ci/evidence/presets/jest.yml +6 -0
  102. data/lib/fun_ci/evidence/presets/logstash.yml +3 -0
  103. data/lib/fun_ci/evidence/presets/maven.yml +6 -0
  104. data/lib/fun_ci/evidence/presets/minitest.yml +6 -0
  105. data/lib/fun_ci/evidence/presets/mocha.yml +6 -0
  106. data/lib/fun_ci/evidence/presets/mypy.yml +6 -0
  107. data/lib/fun_ci/evidence/presets/node-test.yml +6 -0
  108. data/lib/fun_ci/evidence/presets/phpunit.yml +6 -0
  109. data/lib/fun_ci/evidence/presets/pino.yml +5 -0
  110. data/lib/fun_ci/evidence/presets/prove.yml +6 -0
  111. data/lib/fun_ci/evidence/presets/pytest.yml +6 -0
  112. data/lib/fun_ci/evidence/presets/rspec.yml +6 -0
  113. data/lib/fun_ci/evidence/presets/rubocop.yml +6 -0
  114. data/lib/fun_ci/evidence/presets/ruff.yml +6 -0
  115. data/lib/fun_ci/evidence/presets/rustc.yml +6 -0
  116. data/lib/fun_ci/evidence/presets/shellcheck.yml +5 -0
  117. data/lib/fun_ci/evidence/presets/structlog.yml +4 -0
  118. data/lib/fun_ci/evidence/presets/swift-test.yml +5 -0
  119. data/lib/fun_ci/evidence/presets/tsc.yml +6 -0
  120. data/lib/fun_ci/evidence/presets/unittest.yml +6 -0
  121. data/lib/fun_ci/evidence/presets/vitest.yml +6 -0
  122. data/lib/fun_ci/evidence/presets.rb +30 -0
  123. data/lib/fun_ci/evidence/problem.rb +9 -0
  124. data/lib/fun_ci/evidence/process_table.rb +40 -0
  125. data/lib/fun_ci/evidence/saved_run.rb +22 -0
  126. data/lib/fun_ci/evidence/section_walk.rb +49 -0
  127. data/lib/fun_ci/evidence/settings.rb +64 -0
  128. data/lib/fun_ci/evidence/settings_check.rb +78 -0
  129. data/lib/fun_ci/evidence/source.rb +34 -0
  130. data/lib/fun_ci/evidence/sources.rb +21 -0
  131. data/lib/fun_ci/evidence/stage_context.rb +30 -0
  132. data/lib/fun_ci/evidence/stamp.rb +10 -0
  133. data/lib/fun_ci/evidence/start.rb +36 -0
  134. data/lib/fun_ci/evidence/worktree.rb +62 -0
  135. data/lib/fun_ci/persistence/active_runs.rb +69 -0
  136. data/lib/fun_ci/persistence/database.rb +59 -30
  137. data/lib/fun_ci/persistence/null_recorder.rb +25 -0
  138. data/lib/fun_ci/persistence/output_tail.rb +24 -0
  139. data/lib/fun_ci/persistence/pipeline_recorder.rb +78 -32
  140. data/lib/fun_ci/persistence/pipeline_run.rb +26 -17
  141. data/lib/fun_ci/persistence/project_runs.rb +52 -0
  142. data/lib/fun_ci/persistence/raw_outputs.rb +46 -0
  143. data/lib/fun_ci/persistence/retention.rb +25 -0
  144. data/lib/fun_ci/persistence/run_status.rb +31 -0
  145. data/lib/fun_ci/persistence/stage_job.rb +58 -13
  146. data/lib/fun_ci/persistence/state_dir.rb +26 -0
  147. data/lib/fun_ci/persistence/write_trouble.rb +46 -0
  148. data/lib/fun_ci/pipeline/background_fork.rb +42 -0
  149. data/lib/fun_ci/pipeline/command_executor.rb +58 -0
  150. data/lib/fun_ci/pipeline/gate.rb +35 -0
  151. data/lib/fun_ci/pipeline/git_environment.rb +16 -0
  152. data/lib/fun_ci/pipeline/head_part.rb +40 -0
  153. data/lib/fun_ci/pipeline/in_place.rb +17 -0
  154. data/lib/fun_ci/pipeline/output_window.rb +73 -0
  155. data/lib/fun_ci/pipeline/pipeline_forker.rb +18 -16
  156. data/lib/fun_ci/pipeline/process_runner.rb +90 -17
  157. data/lib/fun_ci/pipeline/run_canceller.rb +75 -0
  158. data/lib/fun_ci/pipeline/slot.rb +30 -0
  159. data/lib/fun_ci/pipeline/slot_lock.rb +13 -0
  160. data/lib/fun_ci/pipeline/slot_run.rb +86 -0
  161. data/lib/fun_ci/pipeline/stage_dir.rb +50 -0
  162. data/lib/fun_ci/pipeline/stage_end.rb +70 -0
  163. data/lib/fun_ci/pipeline/stage_execution.rb +67 -0
  164. data/lib/fun_ci/pipeline/stage_runner.rb +42 -65
  165. data/lib/fun_ci/pipeline/stale_pipeline_canceller.rb +16 -33
  166. data/lib/fun_ci/pipeline/tail_segments.rb +60 -0
  167. data/lib/fun_ci/pipeline/test_report.rb +48 -0
  168. data/lib/fun_ci/pipeline/trigger.rb +55 -115
  169. data/lib/fun_ci/pipeline/trigger_command.rb +62 -0
  170. data/lib/fun_ci/pipeline/trigger_params.rb +48 -0
  171. data/lib/fun_ci/pipeline/workspaces.rb +22 -0
  172. data/lib/fun_ci/pipeline/worktree_pool.rb +66 -0
  173. data/lib/fun_ci/pipeline/worktree_prune.rb +49 -0
  174. data/lib/fun_ci/pipeline/worktrees.rb +45 -0
  175. data/lib/fun_ci/setup/agent_instructions.rb +31 -0
  176. data/lib/fun_ci/setup/commands.rb +49 -0
  177. data/lib/fun_ci/setup/git_hooks.rb +20 -0
  178. data/lib/fun_ci/setup/hook_script.rb +47 -0
  179. data/lib/fun_ci/setup/hook_writer.rb +13 -31
  180. data/lib/fun_ci/setup/installer.rb +39 -23
  181. data/lib/fun_ci/setup/legacy_hooks.rb +38 -0
  182. data/lib/fun_ci/setup/project_config.rb +43 -17
  183. data/lib/fun_ci/setup/project_detector.rb +39 -7
  184. data/lib/fun_ci/setup/settings.rb +36 -0
  185. data/lib/fun_ci/setup/setup_checker.rb +18 -12
  186. data/lib/fun_ci/setup/stage_templates/javascript.rb +18 -0
  187. data/lib/fun_ci/setup/stage_templates/managed.rb +24 -0
  188. data/lib/fun_ci/setup/stage_templates/native.rb +25 -0
  189. data/lib/fun_ci/setup/stage_templates/scripting.rb +27 -0
  190. data/lib/fun_ci/setup/stage_templates/shell.rb +16 -0
  191. data/lib/fun_ci/setup/stage_templates.rb +25 -0
  192. data/lib/fun_ci/setup/template_writer.rb +11 -36
  193. data/lib/fun_ci/version.rb +1 -1
  194. data/lib/fun_ci.rb +0 -1
  195. metadata +205 -41
  196. data/exe/fun-ci-trigger +0 -19
  197. data/exe/fun-ci-tui +0 -23
  198. data/lib/fun_ci/animations/celebrate.rb +0 -210
  199. data/lib/fun_ci/animations/explosion.rb +0 -138
  200. data/lib/fun_ci/animations/flash.rb +0 -205
  201. data/lib/fun_ci/animations/idle.rb +0 -127
  202. data/lib/fun_ci/animations/leprechauns.rb +0 -240
  203. data/lib/fun_ci/animations/running.rb +0 -129
  204. data/lib/fun_ci/animations/success.rb +0 -281
  205. data/lib/fun_ci/animations/yay.rb +0 -169
  206. data/lib/fun_ci/persistence/state_machine.rb +0 -50
  207. data/lib/fun_ci/pipeline/background_wrapper.rb +0 -27
  208. data/lib/fun_ci/tui/admin_tui.rb +0 -90
  209. data/lib/fun_ci/tui/animation.rb +0 -49
  210. data/lib/fun_ci/tui/animation_compositor.rb +0 -107
  211. data/lib/fun_ci/tui/animation_frames.rb +0 -112
  212. data/lib/fun_ci/tui/animation_library.rb +0 -46
  213. data/lib/fun_ci/tui/animation_renderer.rb +0 -144
  214. data/lib/fun_ci/tui/ansi.rb +0 -34
  215. data/lib/fun_ci/tui/board_data.rb +0 -53
  216. data/lib/fun_ci/tui/board_renderer.rb +0 -105
  217. data/lib/fun_ci/tui/duration_formatter.rb +0 -24
  218. data/lib/fun_ci/tui/header_animation_manager.rb +0 -71
  219. data/lib/fun_ci/tui/header_animation_player.rb +0 -45
  220. data/lib/fun_ci/tui/key_handler.rb +0 -86
  221. data/lib/fun_ci/tui/looping_animation_player.rb +0 -45
  222. data/lib/fun_ci/tui/relative_time.rb +0 -22
  223. data/lib/fun_ci/tui/row_formatter.rb +0 -108
  224. data/lib/fun_ci/tui/screen.rb +0 -103
  225. data/lib/fun_ci/tui/spinner.rb +0 -24
  226. data/lib/fun_ci/tui/stage_change_detector.rb +0 -58
  227. data/lib/fun_ci/tui/streak_counter.rb +0 -29
  228. data/lib/fun_ci/tui/terminal_input.rb +0 -69
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "shellwords"
4
+ require_relative "settings"
5
+ require_relative "../evidence/settings"
6
+ require_relative "../evidence/start"
7
+
3
8
  module FunCi
4
9
  module Setup
5
10
  class ProjectConfig
@@ -15,28 +20,49 @@ module FunCi
15
20
  end
16
21
 
17
22
  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
23
+ return ["No .fun-ci/ folder found in #{@project_root}"] unless folder_exists?
24
+
25
+ REQUIRED_SCRIPTS.flat_map { |script| script_errors(script) } + settings.errors
35
26
  end
36
27
 
28
+ # Mistakes in the `evidence` key, which `fun-ci check` reports but which
29
+ # never stop a pipeline: the entry with the mistake is left out instead.
30
+ def evidence_errors = Evidence::Settings.load(File.join(@fun_ci_dir, "config")).errors(root: @project_root)
31
+
32
+ # The presets whose markers the project has, which run when a stage fails
33
+ # and its output shows their tool.
34
+ def presets = preset_candidates.select(&:because).map { |found| found.preset.name }
35
+
36
+ # The presets without markers, which read what any project's output
37
+ # may hold, such as JSON logs.
38
+ def any_project_presets = preset_candidates.reject(&:because).map { |found| found.preset.name }
39
+
40
+ def worktree_slots = settings.worktree_slots
41
+
37
42
  def script_path(stage)
38
43
  File.join(@fun_ci_dir, "#{stage}.sh")
39
44
  end
45
+
46
+ # The shell command that runs a stage's script on a commit, its path
47
+ # quoted, since a project's path may hold spaces.
48
+ def stage_command(stage, sha) = "#{Shellwords.escape(script_path(stage))} #{sha}"
49
+
50
+ private
51
+
52
+ def settings = Settings.new(File.join(@fun_ci_dir, "config"))
53
+
54
+ def preset_candidates
55
+ evidence = Evidence::Settings.load(File.join(@fun_ci_dir, "config"))
56
+ Evidence::Start.candidates(Evidence::Worktree.new(@project_root), evidence, "all")
57
+ end
58
+
59
+ def script_errors(script)
60
+ path = File.join(@fun_ci_dir, script)
61
+ return [".fun-ci/#{script} is not found"] unless File.exist?(path)
62
+ return [".fun-ci/#{script} is not executable"] unless File.executable?(path)
63
+
64
+ []
65
+ end
40
66
  end
41
67
  end
42
68
  end
@@ -2,21 +2,53 @@
2
2
 
3
3
  module FunCi
4
4
  module Setup
5
+ # Which kind of project a directory is, from the names at its top: the
6
+ # first stack, in this order, any of whose markers is there. A marker is a
7
+ # name, a glob, or a list of them that must all be there. A language
8
+ # comes before the tooling that sits beside it (Node beside Python, a
9
+ # Makefile around Go), and a lockfile picks the package manager.
5
10
  class ProjectDetector
11
+ STACKS = {
12
+ ruby_rspec: [%w[Gemfile spec], %w[Gemfile .rspec]],
13
+ ruby_bundler: ["Gemfile"],
14
+ jvm_gradle_kotlin: %w[build.gradle.kts settings.gradle.kts],
15
+ jvm_gradle_groovy: %w[build.gradle settings.gradle],
16
+ jvm_maven: ["pom.xml"],
17
+ rust_cargo: ["Cargo.toml"],
18
+ go: ["go.mod"],
19
+ elixir_mix: ["mix.exs"],
20
+ dart: ["pubspec.yaml"],
21
+ swift: ["Package.swift"],
22
+ php_composer: ["composer.json"],
23
+ dotnet: %w[*.sln *.csproj *.fsproj],
24
+ python_uv: ["uv.lock"],
25
+ python_poetry: ["poetry.lock"],
26
+ python: %w[pyproject.toml setup.py setup.cfg requirements.txt],
27
+ deno: %w[deno.json deno.jsonc],
28
+ bun: %w[bun.lock bun.lockb bunfig.toml],
29
+ node_pnpm: ["pnpm-lock.yaml"],
30
+ node_yarn: ["yarn.lock"],
31
+ node_npm: ["package.json"],
32
+ perl: %w[cpanfile Makefile.PL Build.PL dist.ini],
33
+ cmake: ["CMakeLists.txt"],
34
+ make: %w[Makefile makefile GNUmakefile]
35
+ }.freeze
36
+
6
37
  def initialize(filenames)
7
38
  @filenames = filenames
8
39
  end
9
40
 
10
41
  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")
42
+ STACKS.find { |_, markers| markers.any? { |marker| present?(marker) } }&.first || :unknown
43
+ end
17
44
 
18
- :unknown
45
+ private
46
+
47
+ def present?(marker)
48
+ Array(marker).all? { |part| @filenames.any? { |name| names?(part, name) } }
19
49
  end
50
+
51
+ def names?(part, name) = File.fnmatch?(part, name)
20
52
  end
21
53
  end
22
54
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module FunCi
6
+ module Setup
7
+ # .fun-ci/config: settings for this machine's runs, as YAML. Each one left
8
+ # out takes its default; one that is wrong is reported and the default used,
9
+ # as are all of them when the file isn't YAML.
10
+ class Settings
11
+ DEFAULTS = { "worktree_slots" => 2 }.freeze
12
+
13
+ def initialize(path)
14
+ @path = path
15
+ end
16
+
17
+ def worktree_slots = errors.empty? ? values.fetch("worktree_slots") : DEFAULTS.fetch("worktree_slots")
18
+
19
+ def errors
20
+ return [".fun-ci/config must be a mapping such as `worktree_slots: 2`"] unless raw.is_a?(Hash)
21
+
22
+ slots = values["worktree_slots"]
23
+ return [] if slots.is_a?(Integer) && slots.positive?
24
+
25
+ [".fun-ci/config: worktree_slots must be a whole number above 0, not #{slots.inspect}"]
26
+ rescue Psych::SyntaxError => e
27
+ [".fun-ci/config is not YAML: #{e.problem} (line #{e.line})"]
28
+ end
29
+
30
+ private
31
+
32
+ def raw = File.exist?(@path) ? YAML.safe_load_file(@path) || {} : {}
33
+ def values = DEFAULTS.merge(raw)
34
+ end
35
+ end
36
+ end
@@ -1,29 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "project_config"
4
+ require_relative "legacy_hooks"
4
5
 
5
6
  module FunCi
6
7
  module Setup
7
8
  class SetupChecker
8
- def self.run(project_root:, stdout: $stdout, stderr: $stderr)
9
- new(project_root: project_root, stdout: stdout).run
9
+ def self.run(project_root:, stdout: $stdout)
10
+ new(config: ProjectConfig.new(project_root), hooks: LegacyHooks.new(project_root), stdout: stdout).run
10
11
  end
11
12
 
12
- def initialize(project_root:, stdout:)
13
- @config = ProjectConfig.new(project_root)
13
+ def initialize(config:, hooks:, stdout:)
14
+ @config = config
15
+ @hooks = hooks
14
16
  @stdout = stdout
15
17
  end
16
18
 
19
+ # Warnings are printed but don't fail the check.
17
20
  def run
18
- errors = @config.validate
21
+ errors = @config.validate + @config.evidence_errors
22
+ @stdout.puts(errors.empty? ? "All OK. The project is configured." : errors)
23
+ list_presets
24
+ @hooks.warnings.each { |warning| @stdout.puts "Warning: #{warning}" }
25
+ errors.empty? ? 0 : 1
26
+ end
27
+
28
+ private
19
29
 
20
- if errors.empty?
21
- @stdout.puts "All OK. The project is configured."
22
- 0
23
- else
24
- errors.each { |e| @stdout.puts e }
25
- 1
26
- end
30
+ def list_presets
31
+ lists = { "this project" => @config.presets, "any project's output" => @config.any_project_presets }
32
+ lists.each { |what, names| @stdout.puts "Evidence presets for #{what}: #{names.join(", ")}" if names.any? }
27
33
  end
28
34
  end
29
35
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "shell"
4
+
5
+ module FunCi
6
+ module Setup
7
+ # JavaScript and TypeScript: Deno, Bun, and Node with its package managers.
8
+ module StageTemplates
9
+ JAVASCRIPT = {
10
+ deno: stages("deno lint", "deno check", "deno test", "deno task test:slow"),
11
+ bun: stages("bunx eslint .", "bun install", "bun test", "bun run test:slow"),
12
+ node_pnpm: stages("pnpm exec eslint .", "pnpm install", "pnpm test", "pnpm run test:slow"),
13
+ node_yarn: stages("yarn eslint .", "yarn install", "yarn test", "yarn run test:slow"),
14
+ node_npm: stages("npx eslint .", "npm install", "npm test", "npm run test:slow")
15
+ }.freeze
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "shell"
4
+
5
+ module FunCi
6
+ module Setup
7
+ # Stacks that run on a virtual machine: the JVM, .NET, the BEAM and Dart's.
8
+ module StageTemplates
9
+ GRADLE = stages("./gradlew check -x test", "./gradlew assemble", "./gradlew test", "./gradlew integrationTest")
10
+ .freeze
11
+
12
+ MANAGED = {
13
+ jvm_gradle_kotlin: GRADLE,
14
+ jvm_gradle_groovy: GRADLE,
15
+ jvm_maven: stages("mvn verify -DskipTests", "mvn compile", "mvn test", "mvn verify"),
16
+ dotnet: stages("dotnet format --verify-no-changes", "dotnet build",
17
+ 'dotnet test --filter "Category!=Slow"', 'dotnet test --filter "Category=Slow"'),
18
+ elixir_mix: stages("mix format --check-formatted", "mix deps.get && mix compile",
19
+ "mix test --exclude slow", "mix test --only slow"),
20
+ dart: stages("dart analyze", "dart pub get", "dart test --exclude-tags slow", "dart test --tags slow")
21
+ }.freeze
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "shell"
4
+
5
+ module FunCi
6
+ module Setup
7
+ # Stacks that compile to the machine: Rust, Go, Swift, and C or C++ built
8
+ # with CMake or make.
9
+ module StageTemplates
10
+ CTEST = "ctest --test-dir build --output-on-failure --no-tests=error"
11
+ CLANG_FORMAT = "git ls-files -z '*.c' '*.cc' '*.cpp' '*.h' '*.hpp' | xargs -0 clang-format --dry-run --Werror"
12
+
13
+ NATIVE = {
14
+ rust_cargo: stages("cargo clippy --all-targets -- -D warnings", "cargo build --all-targets",
15
+ "cargo test", "cargo test -- --ignored"),
16
+ go: stages("go vet ./...", "go build ./...", "go test -short ./...", "go test ./..."),
17
+ swift: stages("swift format lint --recursive --strict .", "swift build --build-tests",
18
+ "swift test --skip Slow", "swift test --filter Slow"),
19
+ cmake: stages(CLANG_FORMAT, "cmake -S . -B build && cmake --build build",
20
+ "#{CTEST} -LE slow", "#{CTEST} -L slow"),
21
+ make: stages("make lint", "make", "make test", "make test-slow")
22
+ }.freeze
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "shell"
4
+
5
+ module FunCi
6
+ module Setup
7
+ # Interpreted stacks: Ruby, Python, PHP and Perl.
8
+ module StageTemplates
9
+ def self.python(runner, build)
10
+ stages("#{runner}ruff check .", build, "#{runner}pytest -m \"not slow\"", "#{runner}pytest -m slow")
11
+ end
12
+
13
+ SCRIPTING = {
14
+ ruby_rspec: stages("bundle exec rubocop", "bundle install --quiet",
15
+ "bundle exec rspec --tag ~slow", "bundle exec rspec --tag slow"),
16
+ ruby_bundler: stages("bundle exec rubocop", "bundle install --quiet",
17
+ "bundle exec rake test", "bundle exec rake test:slow"),
18
+ php_composer: stages("vendor/bin/phpcs", "composer install --no-interaction --no-progress",
19
+ "vendor/bin/phpunit --exclude-group slow", "vendor/bin/phpunit --group slow"),
20
+ python_uv: python("uv run ", "uv sync"),
21
+ python_poetry: python("poetry run ", "poetry install"),
22
+ python: python("python3 -m ", "python3 -m compileall -q ."),
23
+ perl: stages("perlcritic lib", "cpanm --installdeps --notest .", "prove -lr t", "prove -lr xt")
24
+ }.freeze
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FunCi
4
+ module Setup
5
+ # What every stack's stage scripts are made of.
6
+ module StageTemplates
7
+ def self.script(body) = "#!/bin/sh\n#{body}\n"
8
+
9
+ def self.stages(lint, build, fast, slow)
10
+ { "lint.sh" => lint, "build.sh" => build, "fast.sh" => fast, "slow.sh" => slow }.transform_values do |body|
11
+ script(body)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "stage_templates/managed"
4
+ require_relative "stage_templates/native"
5
+ require_relative "stage_templates/scripting"
6
+ require_relative "stage_templates/javascript"
7
+
8
+ module FunCi
9
+ module Setup
10
+ # The stage scripts `fun-ci init` writes, per kind of project (AT-10.21).
11
+ # Test stages run the fast suite without the tests the tool's convention
12
+ # marks slow, and the slow suite with only those; where the tool has no
13
+ # such convention, the slow suite is a `test:slow` script (`test-slow`,
14
+ # for make).
15
+ module StageTemplates
16
+ TEMPLATES = [MANAGED, NATIVE, SCRIPTING, JAVASCRIPT].reduce(:merge).freeze
17
+
18
+ # { "lint.sh" => script, ... }; +lint_override+ replaces the lint command.
19
+ def self.scripts(template_id, lint_override: nil)
20
+ scripts = TEMPLATES.fetch(template_id)
21
+ lint_override ? scripts.merge("lint.sh" => script(lint_override)) : scripts
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,52 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "stage_templates"
4
+
3
5
  module FunCi
4
6
  module Setup
7
+ # Writes a project's .fun-ci/ stage scripts, executable.
5
8
  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
9
  def initialize(template_id, target_dir, lint_override: nil)
34
- @template_id = template_id
10
+ @scripts = StageTemplates.scripts(template_id, lint_override: lint_override)
35
11
  @target_dir = target_dir
36
- @lint_override = lint_override
37
12
  end
38
13
 
39
14
  def write
40
- scripts = TEMPLATES.fetch(@template_id)
41
15
  fun_ci_dir = File.join(@target_dir, ".fun-ci")
42
16
  Dir.mkdir(fun_ci_dir)
17
+ @scripts.each { |name, content| write_script(File.join(fun_ci_dir, name), content) }
18
+ end
19
+
20
+ private
43
21
 
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
22
+ def write_script(path, content)
23
+ File.write(path, content)
24
+ File.chmod(0o755, path)
50
25
  end
51
26
  end
52
27
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Loaded on its own by the gemspec, so it must not require anything.
4
4
  module FunCi
5
- VERSION = "1.2.1"
5
+ VERSION = "2.0.0"
6
6
  end
data/lib/fun_ci.rb CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  require_relative "fun_ci/version"
4
4
  require_relative "fun_ci/persistence/database"
5
- require_relative "fun_ci/persistence/state_machine"
6
5
  require_relative "fun_ci/persistence/pipeline_run"
7
6
  require_relative "fun_ci/persistence/stage_job"
8
7
  require_relative "fun_ci/setup/project_config"