fun_ci 2.0.0-x86_64-linux
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 +7 -0
- data/CHANGELOG.md +294 -0
- data/LICENSE.txt +21 -0
- data/README.md +230 -0
- data/exe/fun-ci +7 -0
- data/lib/fun_ci/agent/age.rb +15 -0
- data/lib/fun_ci/agent/command_support.rb +27 -0
- data/lib/fun_ci/agent/commands.rb +27 -0
- data/lib/fun_ci/agent/context.rb +10 -0
- data/lib/fun_ci/agent/digest.rb +49 -0
- data/lib/fun_ci/agent/events.rb +47 -0
- data/lib/fun_ci/agent/events_command.rb +61 -0
- data/lib/fun_ci/agent/evidence_text.rb +46 -0
- data/lib/fun_ci/agent/exit_code.rb +11 -0
- data/lib/fun_ci/agent/git.rb +31 -0
- data/lib/fun_ci/agent/live_pipeline.rb +35 -0
- data/lib/fun_ci/agent/options.rb +75 -0
- data/lib/fun_ci/agent/output.rb +81 -0
- data/lib/fun_ci/agent/report_stage.rb +73 -0
- data/lib/fun_ci/agent/reports.rb +47 -0
- data/lib/fun_ci/agent/run_report.rb +30 -0
- data/lib/fun_ci/agent/runs_command.rb +34 -0
- data/lib/fun_ci/agent/runs_text.rb +27 -0
- data/lib/fun_ci/agent/snapshots.rb +39 -0
- data/lib/fun_ci/agent/stage_summary.rb +30 -0
- data/lib/fun_ci/agent/status_command.rb +36 -0
- data/lib/fun_ci/agent/status_json.rb +27 -0
- data/lib/fun_ci/agent/status_text.rb +37 -0
- data/lib/fun_ci/agent/system_clock.rb +11 -0
- data/lib/fun_ci/agent/verdict.rb +32 -0
- data/lib/fun_ci/agent/wait_command.rb +71 -0
- data/lib/fun_ci/agent/waiting.rb +32 -0
- data/lib/fun_ci/agent/why_command.rb +50 -0
- data/lib/fun_ci/agent/why_json.rb +35 -0
- data/lib/fun_ci/agent/why_text.rb +43 -0
- data/lib/fun_ci/cli.rb +128 -0
- data/lib/fun_ci/cli_help.rb +65 -0
- data/lib/fun_ci/console/board_data.rb +66 -0
- data/lib/fun_ci/console/board_heartbeat.rb +40 -0
- data/lib/fun_ci/console/console_events.rb +20 -0
- data/lib/fun_ci/console/console_log.rb +26 -0
- data/lib/fun_ci/console/console_loop.rb +41 -0
- data/lib/fun_ci/console/console_session.rb +82 -0
- data/lib/fun_ci/console/console_state.rb +42 -0
- data/lib/fun_ci/console/key_handler.rb +73 -0
- data/lib/fun_ci/console/launcher.rb +43 -0
- data/lib/fun_ci/console/milestone_events.rb +38 -0
- data/lib/fun_ci/console/milestones.rb +25 -0
- data/lib/fun_ci/console/renderer_lookup.rb +51 -0
- data/lib/fun_ci/console/renderer_process.rb +60 -0
- data/lib/fun_ci/console/run_message.rb +33 -0
- data/lib/fun_ci/console/stage_change_detector.rb +32 -0
- data/lib/fun_ci/console/stage_events.rb +31 -0
- data/lib/fun_ci/console/stop_report.rb +39 -0
- data/lib/fun_ci/console/streak_counter.rb +19 -0
- data/lib/fun_ci/console/view.rb +47 -0
- data/lib/fun_ci/evidence/about.rb +12 -0
- data/lib/fun_ci/evidence/about_stage.rb +29 -0
- data/lib/fun_ci/evidence/caps.rb +77 -0
- data/lib/fun_ci/evidence/catalog.rb +83 -0
- data/lib/fun_ci/evidence/collector.rb +95 -0
- data/lib/fun_ci/evidence/command_output.rb +45 -0
- data/lib/fun_ci/evidence/command_reading.rb +38 -0
- data/lib/fun_ci/evidence/command_runner.rb +75 -0
- data/lib/fun_ci/evidence/context.rb +21 -0
- data/lib/fun_ci/evidence/context_document.rb +29 -0
- data/lib/fun_ci/evidence/deadline.rb +15 -0
- data/lib/fun_ci/evidence/detection.rb +82 -0
- data/lib/fun_ci/evidence/document.rb +59 -0
- data/lib/fun_ci/evidence/extract_command.rb +83 -0
- data/lib/fun_ci/evidence/extract_options.rb +45 -0
- data/lib/fun_ci/evidence/extraction.rb +54 -0
- data/lib/fun_ci/evidence/extractors/command.rb +52 -0
- data/lib/fun_ci/evidence/extractors/grep.rb +49 -0
- data/lib/fun_ci/evidence/extractors/json_log.rb +45 -0
- data/lib/fun_ci/evidence/extractors/junit_files.rb +32 -0
- data/lib/fun_ci/evidence/extractors/last_lines.rb +25 -0
- data/lib/fun_ci/evidence/extractors/log_file.rb +62 -0
- data/lib/fun_ci/evidence/extractors/process_tree.rb +51 -0
- data/lib/fun_ci/evidence/extractors/section.rb +49 -0
- data/lib/fun_ci/evidence/findings.rb +14 -0
- data/lib/fun_ci/evidence/line_ranges.rb +21 -0
- data/lib/fun_ci/evidence/line_scan.rb +22 -0
- data/lib/fun_ci/evidence/log_record.rb +58 -0
- data/lib/fun_ci/evidence/masking.rb +55 -0
- data/lib/fun_ci/evidence/option_check.rb +53 -0
- data/lib/fun_ci/evidence/outcome.rb +15 -0
- data/lib/fun_ci/evidence/patterns.rb +22 -0
- data/lib/fun_ci/evidence/presets/README.md +7 -0
- data/lib/fun_ci/evidence/presets/bun-test.yml +6 -0
- data/lib/fun_ci/evidence/presets/cargo-test.yml +6 -0
- data/lib/fun_ci/evidence/presets/dart-test.yml +6 -0
- data/lib/fun_ci/evidence/presets/deno-test.yml +6 -0
- data/lib/fun_ci/evidence/presets/dotnet-test.yml +6 -0
- data/lib/fun_ci/evidence/presets/ecs.yml +3 -0
- data/lib/fun_ci/evidence/presets/eslint.yml +6 -0
- data/lib/fun_ci/evidence/presets/exunit.yml +6 -0
- data/lib/fun_ci/evidence/presets/gcc.yml +6 -0
- data/lib/fun_ci/evidence/presets/go-build.yml +5 -0
- data/lib/fun_ci/evidence/presets/go-test.yml +6 -0
- data/lib/fun_ci/evidence/presets/gradle.yml +6 -0
- data/lib/fun_ci/evidence/presets/gtest.yml +6 -0
- data/lib/fun_ci/evidence/presets/jest.yml +6 -0
- data/lib/fun_ci/evidence/presets/logstash.yml +3 -0
- data/lib/fun_ci/evidence/presets/maven.yml +6 -0
- data/lib/fun_ci/evidence/presets/minitest.yml +6 -0
- data/lib/fun_ci/evidence/presets/mocha.yml +6 -0
- data/lib/fun_ci/evidence/presets/mypy.yml +6 -0
- data/lib/fun_ci/evidence/presets/node-test.yml +6 -0
- data/lib/fun_ci/evidence/presets/phpunit.yml +6 -0
- data/lib/fun_ci/evidence/presets/pino.yml +5 -0
- data/lib/fun_ci/evidence/presets/prove.yml +6 -0
- data/lib/fun_ci/evidence/presets/pytest.yml +6 -0
- data/lib/fun_ci/evidence/presets/rspec.yml +6 -0
- data/lib/fun_ci/evidence/presets/rubocop.yml +6 -0
- data/lib/fun_ci/evidence/presets/ruff.yml +6 -0
- data/lib/fun_ci/evidence/presets/rustc.yml +6 -0
- data/lib/fun_ci/evidence/presets/shellcheck.yml +5 -0
- data/lib/fun_ci/evidence/presets/structlog.yml +4 -0
- data/lib/fun_ci/evidence/presets/swift-test.yml +5 -0
- data/lib/fun_ci/evidence/presets/tsc.yml +6 -0
- data/lib/fun_ci/evidence/presets/unittest.yml +6 -0
- data/lib/fun_ci/evidence/presets/vitest.yml +6 -0
- data/lib/fun_ci/evidence/presets.rb +30 -0
- data/lib/fun_ci/evidence/problem.rb +9 -0
- data/lib/fun_ci/evidence/process_table.rb +40 -0
- data/lib/fun_ci/evidence/saved_run.rb +22 -0
- data/lib/fun_ci/evidence/section_walk.rb +49 -0
- data/lib/fun_ci/evidence/settings.rb +64 -0
- data/lib/fun_ci/evidence/settings_check.rb +78 -0
- data/lib/fun_ci/evidence/source.rb +34 -0
- data/lib/fun_ci/evidence/sources.rb +21 -0
- data/lib/fun_ci/evidence/stage_context.rb +30 -0
- data/lib/fun_ci/evidence/stamp.rb +10 -0
- data/lib/fun_ci/evidence/start.rb +36 -0
- data/lib/fun_ci/evidence/worktree.rb +62 -0
- data/lib/fun_ci/persistence/active_runs.rb +69 -0
- data/lib/fun_ci/persistence/database.rb +81 -0
- data/lib/fun_ci/persistence/null_recorder.rb +25 -0
- data/lib/fun_ci/persistence/output_tail.rb +24 -0
- data/lib/fun_ci/persistence/pipeline_recorder.rb +120 -0
- data/lib/fun_ci/persistence/pipeline_run.rb +68 -0
- data/lib/fun_ci/persistence/project_runs.rb +52 -0
- data/lib/fun_ci/persistence/raw_outputs.rb +46 -0
- data/lib/fun_ci/persistence/retention.rb +25 -0
- data/lib/fun_ci/persistence/run_status.rb +31 -0
- data/lib/fun_ci/persistence/stage_job.rb +91 -0
- data/lib/fun_ci/persistence/state_dir.rb +26 -0
- data/lib/fun_ci/persistence/write_trouble.rb +46 -0
- data/lib/fun_ci/pipeline/background_fork.rb +42 -0
- data/lib/fun_ci/pipeline/command_executor.rb +58 -0
- data/lib/fun_ci/pipeline/gate.rb +35 -0
- data/lib/fun_ci/pipeline/git_environment.rb +16 -0
- data/lib/fun_ci/pipeline/head_part.rb +40 -0
- data/lib/fun_ci/pipeline/in_place.rb +17 -0
- data/lib/fun_ci/pipeline/output_window.rb +73 -0
- data/lib/fun_ci/pipeline/pipeline_forker.rb +40 -0
- data/lib/fun_ci/pipeline/process_runner.rb +101 -0
- data/lib/fun_ci/pipeline/progress_reporter.rb +31 -0
- data/lib/fun_ci/pipeline/run_canceller.rb +75 -0
- data/lib/fun_ci/pipeline/slot.rb +30 -0
- data/lib/fun_ci/pipeline/slot_lock.rb +13 -0
- data/lib/fun_ci/pipeline/slot_run.rb +86 -0
- data/lib/fun_ci/pipeline/stage_dir.rb +50 -0
- data/lib/fun_ci/pipeline/stage_end.rb +70 -0
- data/lib/fun_ci/pipeline/stage_execution.rb +67 -0
- data/lib/fun_ci/pipeline/stage_runner.rb +63 -0
- data/lib/fun_ci/pipeline/stale_pipeline_canceller.rb +36 -0
- data/lib/fun_ci/pipeline/tail_segments.rb +60 -0
- data/lib/fun_ci/pipeline/test_report.rb +48 -0
- data/lib/fun_ci/pipeline/trigger.rb +93 -0
- data/lib/fun_ci/pipeline/trigger_command.rb +62 -0
- data/lib/fun_ci/pipeline/trigger_params.rb +48 -0
- data/lib/fun_ci/pipeline/workspaces.rb +22 -0
- data/lib/fun_ci/pipeline/worktree_pool.rb +66 -0
- data/lib/fun_ci/pipeline/worktree_prune.rb +49 -0
- data/lib/fun_ci/pipeline/worktrees.rb +45 -0
- data/lib/fun_ci/setup/agent_instructions.rb +31 -0
- data/lib/fun_ci/setup/commands.rb +49 -0
- data/lib/fun_ci/setup/git_hooks.rb +20 -0
- data/lib/fun_ci/setup/hook_script.rb +47 -0
- data/lib/fun_ci/setup/hook_writer.rb +57 -0
- data/lib/fun_ci/setup/installer.rb +71 -0
- data/lib/fun_ci/setup/legacy_hooks.rb +38 -0
- data/lib/fun_ci/setup/maven_linter_detector.rb +26 -0
- data/lib/fun_ci/setup/project_config.rb +68 -0
- data/lib/fun_ci/setup/project_detector.rb +54 -0
- data/lib/fun_ci/setup/settings.rb +36 -0
- data/lib/fun_ci/setup/setup_checker.rb +36 -0
- data/lib/fun_ci/setup/stage_templates/javascript.rb +18 -0
- data/lib/fun_ci/setup/stage_templates/managed.rb +24 -0
- data/lib/fun_ci/setup/stage_templates/native.rb +25 -0
- data/lib/fun_ci/setup/stage_templates/scripting.rb +27 -0
- data/lib/fun_ci/setup/stage_templates/shell.rb +16 -0
- data/lib/fun_ci/setup/stage_templates.rb +25 -0
- data/lib/fun_ci/setup/template_writer.rb +28 -0
- data/lib/fun_ci/version.rb +6 -0
- data/lib/fun_ci.rb +13 -0
- data/libexec/fun-ci-renderer +0 -0
- metadata +286 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Setup
|
|
5
|
+
# What `fun-ci init` tells a coding agent working in the project, in the
|
|
6
|
+
# instructions file agents read (acceptance-tests.md, AT-9.15): AGENTS.md,
|
|
7
|
+
# or CLAUDE.md when that is the only one. The marker keeps it to once.
|
|
8
|
+
module AgentInstructions
|
|
9
|
+
MARKER = "<!-- fun-ci: agent instructions -->"
|
|
10
|
+
SECTION = <<~MD.freeze
|
|
11
|
+
## fun-ci
|
|
12
|
+
|
|
13
|
+
#{MARKER}
|
|
14
|
+
This project runs fun-ci on every commit. After each commit, run the
|
|
15
|
+
`fun-ci wait` command it prints, in the background, and act on its exit
|
|
16
|
+
code: 0 passed, 1 failed, 2 over budget, 3 undecided, 4 superseded.
|
|
17
|
+
`fun-ci wait --need all` must exit 0 before the work is called done.
|
|
18
|
+
`fun-ci runs` lists recent runs, and `fun-ci status` says where one stands.
|
|
19
|
+
MD
|
|
20
|
+
|
|
21
|
+
def self.file_for(names) = names.include?("CLAUDE.md") && !names.include?("AGENTS.md") ? "CLAUDE.md" : "AGENTS.md"
|
|
22
|
+
|
|
23
|
+
# The text with the section added, or nil when it has it already.
|
|
24
|
+
def self.merged(text)
|
|
25
|
+
return nil if text.include?(MARKER)
|
|
26
|
+
|
|
27
|
+
text.strip.empty? ? SECTION : "#{text.rstrip}\n\n#{SECTION}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "installer"
|
|
4
|
+
require_relative "hook_writer"
|
|
5
|
+
require_relative "git_hooks"
|
|
6
|
+
require_relative "setup_checker"
|
|
7
|
+
require_relative "legacy_hooks"
|
|
8
|
+
|
|
9
|
+
module FunCi
|
|
10
|
+
module Setup
|
|
11
|
+
# `fun-ci init`, `install-hooks` and `check`, in the project directory.
|
|
12
|
+
# `hooks_dir` answers where git runs a project's hooks from.
|
|
13
|
+
class Commands
|
|
14
|
+
def initialize(project_root, stdout, hooks_dir: GitHooks.method(:dir))
|
|
15
|
+
@project_root = project_root
|
|
16
|
+
@stdout = stdout
|
|
17
|
+
@hooks_dir = hooks_dir
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def init(args)
|
|
21
|
+
code = Installer.run(project_root: @project_root, stdout: @stdout)
|
|
22
|
+
return code if !code.zero? || !args.include?("--everything")
|
|
23
|
+
|
|
24
|
+
code = install_hooks([])
|
|
25
|
+
return code unless code.zero?
|
|
26
|
+
|
|
27
|
+
check([])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def install_hooks(args)
|
|
31
|
+
LegacyHooks.new(@project_root).remove(@stdout)
|
|
32
|
+
install(args.any? ? [args.first] : HookScript.types)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def check(_args) = SetupChecker.run(project_root: @project_root, stdout: @stdout)
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def install(types)
|
|
40
|
+
hooks_dir = @hooks_dir.call(@project_root)
|
|
41
|
+
types.each do |type|
|
|
42
|
+
code = HookWriter.run(hooks_dir: hooks_dir, hook_type: type, stdout: @stdout)
|
|
43
|
+
return code unless code.zero?
|
|
44
|
+
end
|
|
45
|
+
0
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require_relative "../pipeline/git_environment"
|
|
5
|
+
|
|
6
|
+
module FunCi
|
|
7
|
+
module Setup
|
|
8
|
+
# Where git runs a project's hooks from: its git directory's hooks, or
|
|
9
|
+
# where core.hooksPath points, and from a linked worktree the
|
|
10
|
+
# repository's own. Git is asked, as .git may be a file or be ignored.
|
|
11
|
+
module GitHooks
|
|
12
|
+
# The directory, or nil outside a git repository.
|
|
13
|
+
def self.dir(project_root)
|
|
14
|
+
out, status = Open3.capture2(Pipeline::GitEnvironment::CLEAN, "git", "rev-parse", "--git-path", "hooks",
|
|
15
|
+
chdir: project_root, err: File::NULL)
|
|
16
|
+
status.success? ? File.expand_path(out.strip, project_root) : nil
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Setup
|
|
5
|
+
# The script fun-ci writes into each git hook it manages.
|
|
6
|
+
module HookScript
|
|
7
|
+
MARKER = "# fun-ci-managed-hook"
|
|
8
|
+
NULL_SHA = "0" * 40
|
|
9
|
+
|
|
10
|
+
GUARD = <<~SH.freeze
|
|
11
|
+
#!/bin/sh
|
|
12
|
+
#{MARKER}
|
|
13
|
+
if ! command -v fun-ci >/dev/null 2>&1; then
|
|
14
|
+
echo "fun-ci is not installed or not on PATH, so this runs without CI. Install it with: gem install fun_ci" >&2
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
SH
|
|
18
|
+
|
|
19
|
+
# After a commit: the commit just made, in the background.
|
|
20
|
+
POST_COMMIT = <<~SH.freeze
|
|
21
|
+
COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "#{NULL_SHA}")
|
|
22
|
+
BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
|
23
|
+
fun-ci trigger --background "$COMMIT" "$BRANCH"
|
|
24
|
+
SH
|
|
25
|
+
|
|
26
|
+
# Before a push: the fast verdict of each commit git says it pushes
|
|
27
|
+
# (acceptance-tests.md, AT-9.13). A deleted ref pushes the null SHA, and
|
|
28
|
+
# exit 5 is a project not set up for fun-ci, which pushes without CI.
|
|
29
|
+
PRE_PUSH = <<~SH.freeze
|
|
30
|
+
status=0
|
|
31
|
+
while read -r local_ref local_sha remote_ref remote_sha; do
|
|
32
|
+
[ "$local_sha" = "#{NULL_SHA}" ] && continue
|
|
33
|
+
fun-ci wait "$local_sha" --need fast
|
|
34
|
+
code=$?
|
|
35
|
+
[ "$code" -eq 0 ] || [ "$code" -eq 5 ] || status=1
|
|
36
|
+
done
|
|
37
|
+
exit $status
|
|
38
|
+
SH
|
|
39
|
+
|
|
40
|
+
BODIES = { "post-commit" => POST_COMMIT, "pre-push" => PRE_PUSH }.freeze
|
|
41
|
+
|
|
42
|
+
def self.types = BODIES.keys
|
|
43
|
+
def self.for(hook_type) = GUARD + BODIES.fetch(hook_type)
|
|
44
|
+
def self.managed?(script) = script.include?(MARKER)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "hook_script"
|
|
5
|
+
|
|
6
|
+
module FunCi
|
|
7
|
+
module Setup
|
|
8
|
+
# Writes a hook into `hooks_dir`, where git runs the project's hooks
|
|
9
|
+
# from (GitHooks.dir); nil outside a git repository.
|
|
10
|
+
class HookWriter
|
|
11
|
+
def self.run(hooks_dir:, hook_type:, stdout: $stdout)
|
|
12
|
+
new(hooks_dir: hooks_dir, hook_type: hook_type, stdout: stdout).run
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(hooks_dir:, hook_type:, stdout:)
|
|
16
|
+
@hooks_dir = hooks_dir
|
|
17
|
+
@hook_type = hook_type
|
|
18
|
+
@stdout = stdout
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run
|
|
22
|
+
return reject("Not a git repository: no .git/ found.") unless git_repo?
|
|
23
|
+
return reject("Unknown hook type: #{@hook_type}") unless HookScript.types.include?(@hook_type)
|
|
24
|
+
return skip("Hook #{@hook_type} already exists from another tool, so it was left alone.") if foreign_hook?
|
|
25
|
+
|
|
26
|
+
write_hook
|
|
27
|
+
@stdout.puts "Installed #{@hook_type} hook."
|
|
28
|
+
0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def git_repo? = !@hooks_dir.nil?
|
|
34
|
+
def hook_path = File.join(@hooks_dir, @hook_type)
|
|
35
|
+
|
|
36
|
+
def foreign_hook?
|
|
37
|
+
File.exist?(hook_path) && !HookScript.managed?(File.read(hook_path))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def write_hook
|
|
41
|
+
FileUtils.mkdir_p(@hooks_dir)
|
|
42
|
+
File.write(hook_path, HookScript.for(@hook_type))
|
|
43
|
+
File.chmod(0o755, hook_path)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def reject(message)
|
|
47
|
+
@stdout.puts message
|
|
48
|
+
1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def skip(message)
|
|
52
|
+
@stdout.puts message
|
|
53
|
+
0
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "project_detector"
|
|
4
|
+
require_relative "template_writer"
|
|
5
|
+
require_relative "maven_linter_detector"
|
|
6
|
+
require_relative "agent_instructions"
|
|
7
|
+
|
|
8
|
+
module FunCi
|
|
9
|
+
module Setup
|
|
10
|
+
class Installer
|
|
11
|
+
def self.run(project_root:, stdout: $stdout)
|
|
12
|
+
new(project_root: project_root, stdout: stdout).run
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(project_root:, stdout:)
|
|
16
|
+
@project_root = project_root
|
|
17
|
+
@stdout = stdout
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# A project set up for fun-ci, new or not, also tells agents what to do.
|
|
21
|
+
def run
|
|
22
|
+
code = set_up
|
|
23
|
+
tell_agents if code.zero?
|
|
24
|
+
code
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def set_up
|
|
30
|
+
return report(".fun-ci/ already exists, so init left it as it is.", 0) if initialised?
|
|
31
|
+
|
|
32
|
+
detected = ProjectDetector.new(Dir.children(@project_root)).detect
|
|
33
|
+
return report("Could not detect project type. Create .fun-ci/ manually.", 1) if detected == :unknown
|
|
34
|
+
|
|
35
|
+
write_templates(detected)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def tell_agents
|
|
39
|
+
file = AgentInstructions.file_for(Dir.children(@project_root))
|
|
40
|
+
path = File.join(@project_root, file)
|
|
41
|
+
merged = AgentInstructions.merged(File.exist?(path) ? File.read(path) : "")
|
|
42
|
+
return unless merged
|
|
43
|
+
|
|
44
|
+
File.write(path, merged)
|
|
45
|
+
@stdout.puts "Told agents what to do after a commit, in #{file}."
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def initialised?
|
|
49
|
+
Dir.exist?(File.join(@project_root, ".fun-ci"))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def write_templates(detected)
|
|
53
|
+
@stdout.puts "Detected: #{detected.to_s.tr("_", " ")}"
|
|
54
|
+
TemplateWriter.new(detected, @project_root, lint_override: detect_maven_linter(detected)).write
|
|
55
|
+
report("Created .fun-ci/ with template scripts.", 0)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def report(message, exit_code)
|
|
59
|
+
@stdout.puts message
|
|
60
|
+
exit_code
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def detect_maven_linter(detected)
|
|
64
|
+
return nil unless detected == :jvm_maven
|
|
65
|
+
|
|
66
|
+
command = MavenLinterDetector.new(File.read(File.join(@project_root, "pom.xml"))).lint_command
|
|
67
|
+
command == MavenLinterDetector::DEFAULT_COMMAND ? nil : command
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "hook_script"
|
|
4
|
+
|
|
5
|
+
module FunCi
|
|
6
|
+
module Setup
|
|
7
|
+
# fun-ci 1.x ran the background pipeline from a pre-commit hook, before
|
|
8
|
+
# the commit existed. The hook fun-ci wrote goes on upgrade, and is warned
|
|
9
|
+
# about until then; one someone wrote by hand that still calls fun-ci is
|
|
10
|
+
# warned about, never touched.
|
|
11
|
+
class LegacyHooks
|
|
12
|
+
WARNING = ".git/hooks/pre-commit calls fun-ci, which now runs after the commit: " \
|
|
13
|
+
"take fun-ci out of it and run `fun-ci install-hooks`"
|
|
14
|
+
NOT_UPGRADED = "fun-ci 1.x's pre-commit hook is still installed, so each run tests the commit before yours " \
|
|
15
|
+
"and yours gets none: run `fun-ci install-hooks`"
|
|
16
|
+
|
|
17
|
+
def initialize(project_root)
|
|
18
|
+
@hook = File.join(project_root, ".git", "hooks", "pre-commit")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def remove(stdout)
|
|
22
|
+
return unless File.exist?(@hook) && HookScript.managed?(File.read(@hook))
|
|
23
|
+
|
|
24
|
+
File.delete(@hook)
|
|
25
|
+
stdout.puts "Removed fun-ci's 1.x pre-commit hook; the background pipeline now runs from post-commit."
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def warnings
|
|
29
|
+
return [] unless File.exist?(@hook)
|
|
30
|
+
|
|
31
|
+
script = File.read(@hook)
|
|
32
|
+
return [NOT_UPGRADED] if HookScript.managed?(script)
|
|
33
|
+
|
|
34
|
+
script.include?("fun-ci") ? [WARNING] : []
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
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,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
require_relative "settings"
|
|
5
|
+
require_relative "../evidence/settings"
|
|
6
|
+
require_relative "../evidence/start"
|
|
7
|
+
|
|
8
|
+
module FunCi
|
|
9
|
+
module Setup
|
|
10
|
+
class ProjectConfig
|
|
11
|
+
REQUIRED_SCRIPTS = %w[lint.sh build.sh fast.sh slow.sh].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(project_root)
|
|
14
|
+
@project_root = project_root
|
|
15
|
+
@fun_ci_dir = File.join(project_root, ".fun-ci")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def folder_exists?
|
|
19
|
+
Dir.exist?(@fun_ci_dir)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def validate
|
|
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
|
|
26
|
+
end
|
|
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
|
+
|
|
42
|
+
def script_path(stage)
|
|
43
|
+
File.join(@fun_ci_dir, "#{stage}.sh")
|
|
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
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
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.
|
|
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
|
+
|
|
37
|
+
def initialize(filenames)
|
|
38
|
+
@filenames = filenames
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def detect
|
|
42
|
+
STACKS.find { |_, markers| markers.any? { |marker| present?(marker) } }&.first || :unknown
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def present?(marker)
|
|
48
|
+
Array(marker).all? { |part| @filenames.any? { |name| names?(part, name) } }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def names?(part, name) = File.fnmatch?(part, name)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
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
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "project_config"
|
|
4
|
+
require_relative "legacy_hooks"
|
|
5
|
+
|
|
6
|
+
module FunCi
|
|
7
|
+
module Setup
|
|
8
|
+
class SetupChecker
|
|
9
|
+
def self.run(project_root:, stdout: $stdout)
|
|
10
|
+
new(config: ProjectConfig.new(project_root), hooks: LegacyHooks.new(project_root), stdout: stdout).run
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(config:, hooks:, stdout:)
|
|
14
|
+
@config = config
|
|
15
|
+
@hooks = hooks
|
|
16
|
+
@stdout = stdout
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Warnings are printed but don't fail the check.
|
|
20
|
+
def run
|
|
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
|
|
29
|
+
|
|
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? }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
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
|