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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +218 -0
- data/README.md +180 -59
- 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 +72 -94
- 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 +59 -30
- 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 +78 -32
- data/lib/fun_ci/persistence/pipeline_run.rb +26 -17
- 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 +58 -13
- 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 +18 -16
- data/lib/fun_ci/pipeline/process_runner.rb +90 -17
- 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 +42 -65
- data/lib/fun_ci/pipeline/stale_pipeline_canceller.rb +16 -33
- 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 +55 -115
- 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 +13 -31
- data/lib/fun_ci/setup/installer.rb +39 -23
- data/lib/fun_ci/setup/legacy_hooks.rb +38 -0
- data/lib/fun_ci/setup/project_config.rb +43 -17
- data/lib/fun_ci/setup/project_detector.rb +39 -7
- data/lib/fun_ci/setup/settings.rb +36 -0
- data/lib/fun_ci/setup/setup_checker.rb +18 -12
- 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 +11 -36
- data/lib/fun_ci/version.rb +1 -1
- data/lib/fun_ci.rb +0 -1
- metadata +205 -41
- data/exe/fun-ci-trigger +0 -19
- data/exe/fun-ci-tui +0 -23
- data/lib/fun_ci/animations/celebrate.rb +0 -210
- data/lib/fun_ci/animations/explosion.rb +0 -138
- data/lib/fun_ci/animations/flash.rb +0 -205
- data/lib/fun_ci/animations/idle.rb +0 -127
- data/lib/fun_ci/animations/leprechauns.rb +0 -240
- data/lib/fun_ci/animations/running.rb +0 -129
- data/lib/fun_ci/animations/success.rb +0 -281
- data/lib/fun_ci/animations/yay.rb +0 -169
- data/lib/fun_ci/persistence/state_machine.rb +0 -50
- data/lib/fun_ci/pipeline/background_wrapper.rb +0 -27
- data/lib/fun_ci/tui/admin_tui.rb +0 -90
- data/lib/fun_ci/tui/animation.rb +0 -49
- data/lib/fun_ci/tui/animation_compositor.rb +0 -107
- data/lib/fun_ci/tui/animation_frames.rb +0 -112
- data/lib/fun_ci/tui/animation_library.rb +0 -46
- data/lib/fun_ci/tui/animation_renderer.rb +0 -144
- data/lib/fun_ci/tui/ansi.rb +0 -34
- data/lib/fun_ci/tui/board_data.rb +0 -53
- data/lib/fun_ci/tui/board_renderer.rb +0 -105
- data/lib/fun_ci/tui/duration_formatter.rb +0 -24
- data/lib/fun_ci/tui/header_animation_manager.rb +0 -71
- data/lib/fun_ci/tui/header_animation_player.rb +0 -45
- data/lib/fun_ci/tui/key_handler.rb +0 -86
- data/lib/fun_ci/tui/looping_animation_player.rb +0 -45
- data/lib/fun_ci/tui/relative_time.rb +0 -22
- data/lib/fun_ci/tui/row_formatter.rb +0 -108
- data/lib/fun_ci/tui/screen.rb +0 -103
- data/lib/fun_ci/tui/spinner.rb +0 -24
- data/lib/fun_ci/tui/stage_change_detector.rb +0 -58
- data/lib/fun_ci/tui/streak_counter.rb +0 -29
- data/lib/fun_ci/tui/terminal_input.rb +0 -69
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "trigger_params"
|
|
4
|
+
require_relative "pipeline_forker"
|
|
5
|
+
|
|
6
|
+
module FunCi
|
|
7
|
+
module Pipeline
|
|
8
|
+
# `fun-ci trigger [--background] <commit-hash> <branch>`. --no-validate is
|
|
9
|
+
# the 1.x name for --background, kept for one release.
|
|
10
|
+
class TriggerCommand
|
|
11
|
+
BACKGROUND = %w[--background --no-validate].freeze
|
|
12
|
+
DEPRECATED = "fun-ci: --no-validate is now --background; the old name goes in 2.1."
|
|
13
|
+
|
|
14
|
+
def initialize(io:, recorder:, pipeline_forker:, project: Dir.pwd)
|
|
15
|
+
@io = io
|
|
16
|
+
@recorder = recorder
|
|
17
|
+
@pipeline_forker = pipeline_forker || PipelineForker.method(:fork_pipeline)
|
|
18
|
+
@project = project
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Closes the database connection it was given on every path.
|
|
22
|
+
def run(args)
|
|
23
|
+
sha, branch = args.reject { |a| a.start_with?("--") }
|
|
24
|
+
return usage unless branch
|
|
25
|
+
|
|
26
|
+
@io.stderr.puts DEPRECATED if args.include?("--no-validate")
|
|
27
|
+
commit = Commit.new(sha: sha, branch: branch)
|
|
28
|
+
args.intersect?(BACKGROUND) ? fork_pipeline(commit) : run_pipeline(commit)
|
|
29
|
+
ensure
|
|
30
|
+
@recorder.close
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# After forking the slow suite the trigger holds a recorder of its own.
|
|
36
|
+
def run_pipeline(commit)
|
|
37
|
+
trigger = Trigger.new(project: @project, commit: commit, io: @io, seams: Seams.new(recorder: @recorder))
|
|
38
|
+
trigger.run
|
|
39
|
+
ensure
|
|
40
|
+
trigger&.close
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def usage
|
|
44
|
+
@io.stderr.puts "fun-ci: commit hash and branch name are required."
|
|
45
|
+
@io.stderr.puts "Usage: fun-ci trigger <commit-hash> <branch>"
|
|
46
|
+
1
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The forker answers whether it started a run; one that did says how an
|
|
50
|
+
# agent gets its verdict (acceptance-tests.md, AT-9.14).
|
|
51
|
+
def fork_pipeline(commit)
|
|
52
|
+
db_path = @recorder.db_path
|
|
53
|
+
@recorder.close
|
|
54
|
+
started = @pipeline_forker.call(commit_hash: commit.sha, branch: commit.branch, db_path: db_path)
|
|
55
|
+
say_how_to_wait(commit.sha[0, 7]) if started
|
|
56
|
+
0
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def say_how_to_wait(sha) = @io.stdout.puts("fun-ci: testing #{sha}. Verdict: fun-ci wait #{sha} --need all")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require_relative "../persistence/pipeline_recorder"
|
|
5
|
+
require_relative "command_executor"
|
|
6
|
+
require_relative "stage_dir"
|
|
7
|
+
require_relative "git_environment"
|
|
8
|
+
require_relative "../evidence/command_runner"
|
|
9
|
+
require_relative "../evidence/process_table"
|
|
10
|
+
|
|
11
|
+
module FunCi
|
|
12
|
+
module Pipeline
|
|
13
|
+
DEFAULT_BUDGETS = { "lint" => 30, "build" => 30, "fast" => 10, "slow" => 300 }.freeze
|
|
14
|
+
|
|
15
|
+
Commit = Data.define(:sha, :branch)
|
|
16
|
+
|
|
17
|
+
Io = Data.define(:stdout, :stderr) do
|
|
18
|
+
def initialize(stdout: $stdout, stderr: $stderr) = super
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The collaborators a pipeline run can have replaced. Each one left out
|
|
22
|
+
# gets the real thing. `environment` is the stages' environment as a hash,
|
|
23
|
+
# which masking and injected command runners see; `clock` answers seconds
|
|
24
|
+
# for the evidence budget; `extractor_runner` makes what runs a project's
|
|
25
|
+
# own extractors (`run:` entries), given dir:, env: and scratch:;
|
|
26
|
+
# `process_table` answers what ps lists, for a stage over budget.
|
|
27
|
+
Seams = Data.define(:command_runner, :time_budgets, :commit_validator, :recorder, :background_launcher, :workspace,
|
|
28
|
+
:stage_dir, :environment, :clock, :extractor_runner, :process_table)
|
|
29
|
+
|
|
30
|
+
# Reopened rather than given as a block to Data.define, so tools that read
|
|
31
|
+
# the source (mutineer) see these as Seams' methods.
|
|
32
|
+
class Seams
|
|
33
|
+
def self.defaults
|
|
34
|
+
{ command_runner: nil, time_budgets: {}, recorder: Persistence::NullRecorder.new, background_launcher: nil,
|
|
35
|
+
workspace: nil, commit_validator: method(:commit_exists?), stage_dir: StageDir.method(:create),
|
|
36
|
+
environment: ENV.to_h, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) },
|
|
37
|
+
extractor_runner: Evidence::CommandRunner.method(:new),
|
|
38
|
+
process_table: Evidence::ProcessTable.method(:now) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.commit_exists?(sha) = Open3.capture2e(GitEnvironment::CLEAN, "git", "cat-file", "-t", sha).last.success?
|
|
42
|
+
|
|
43
|
+
def initialize(**given) = super(**self.class.defaults.merge(given))
|
|
44
|
+
def budgets = DEFAULT_BUDGETS.merge(time_budgets)
|
|
45
|
+
def executor(dir) = CommandExecutor.new(command_runner, dir, environment)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../setup/project_config"
|
|
4
|
+
require_relative "worktree_pool"
|
|
5
|
+
require_relative "in_place"
|
|
6
|
+
|
|
7
|
+
module FunCi
|
|
8
|
+
module Pipeline
|
|
9
|
+
# Where a pipeline for a commit runs: a slot from the project's worktree
|
|
10
|
+
# pool, sized by .fun-ci/config, or, for a root commit's null SHA, which
|
|
11
|
+
# no worktree can check out, the project directory itself.
|
|
12
|
+
module Workspaces
|
|
13
|
+
NULL_SHA = ("0" * 40).freeze
|
|
14
|
+
|
|
15
|
+
def self.for(project, sha)
|
|
16
|
+
return InPlace.new(project) if sha == NULL_SHA
|
|
17
|
+
|
|
18
|
+
WorktreePool.new(Worktrees.new(project), size: Setup::ProjectConfig.new(project).worktree_slots)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "slot"
|
|
5
|
+
require_relative "worktrees"
|
|
6
|
+
|
|
7
|
+
module FunCi
|
|
8
|
+
module Pipeline
|
|
9
|
+
# Hands each pipeline a worktree slot of its own under
|
|
10
|
+
# <git-common-dir>/fun-ci/worktrees, checked out at its commit, waiting
|
|
11
|
+
# while every slot is taken.
|
|
12
|
+
class WorktreePool
|
|
13
|
+
DEFAULT_SIZE = 2
|
|
14
|
+
|
|
15
|
+
attr_reader :size
|
|
16
|
+
|
|
17
|
+
def initialize(worktrees, size: DEFAULT_SIZE, waiter: -> { sleep 0.25 })
|
|
18
|
+
@worktrees = worktrees
|
|
19
|
+
@size = size
|
|
20
|
+
@waiter = waiter
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def acquire(sha)
|
|
24
|
+
slot = wait_for_free_slot
|
|
25
|
+
@worktrees.check_out(slot.path, sha)
|
|
26
|
+
slot
|
|
27
|
+
rescue StandardError
|
|
28
|
+
slot&.release
|
|
29
|
+
raise
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def wait_for_free_slot
|
|
35
|
+
loop do
|
|
36
|
+
slot = free_slot
|
|
37
|
+
return slot if slot
|
|
38
|
+
|
|
39
|
+
@waiter.call
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def free_slot
|
|
44
|
+
root = @worktrees.root
|
|
45
|
+
FileUtils.mkdir_p(root)
|
|
46
|
+
@size.times.lazy.filter_map { |index| try_lock(File.join(root, "slot-#{index}")) }.first
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The lock stays open for as long as the slot is held, so no block.
|
|
50
|
+
def try_lock(path)
|
|
51
|
+
lock = File.new("#{path}.lock", File::RDWR | File::CREAT, 0o644)
|
|
52
|
+
return Slot.new(path, stamped(lock), lock.path) if lock.flock(File::LOCK_EX | File::LOCK_NB)
|
|
53
|
+
|
|
54
|
+
lock.close
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def stamped(lock)
|
|
59
|
+
lock.truncate(0)
|
|
60
|
+
lock.write(Process.pid.to_s)
|
|
61
|
+
lock.flush
|
|
62
|
+
lock
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "worktrees"
|
|
5
|
+
|
|
6
|
+
module FunCi
|
|
7
|
+
module Pipeline
|
|
8
|
+
# Removes every slot of a project's worktree pool, and git's entries for
|
|
9
|
+
# them, when no run holds one. It takes each slot's lock first, so a run
|
|
10
|
+
# starting meanwhile waits until the slot is gone and then checks it out
|
|
11
|
+
# afresh.
|
|
12
|
+
class WorktreePrune
|
|
13
|
+
class Busy < StandardError; end
|
|
14
|
+
|
|
15
|
+
def initialize(worktrees)
|
|
16
|
+
@worktrees = worktrees
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# How many slots it removed. Raises Busy, removing nothing, while a run
|
|
20
|
+
# holds a slot.
|
|
21
|
+
def run
|
|
22
|
+
locks = take_locks
|
|
23
|
+
locks.each { |lock| FileUtils.rm_rf(lock.path.delete_suffix(".lock")) }
|
|
24
|
+
@worktrees.prune
|
|
25
|
+
locks.size
|
|
26
|
+
ensure
|
|
27
|
+
locks&.each(&:close)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Every slot's lock, or none: on the first slot a run holds, it lets go
|
|
33
|
+
# of the locks it took before raising Busy.
|
|
34
|
+
def take_locks
|
|
35
|
+
slot_paths.each_with_object([]) do |path, locks|
|
|
36
|
+
locks << File.new("#{path}.lock", File::RDWR | File::CREAT, 0o644)
|
|
37
|
+
next if locks.last.flock(File::LOCK_EX | File::LOCK_NB)
|
|
38
|
+
|
|
39
|
+
locks.each(&:close)
|
|
40
|
+
raise Busy, "a pipeline is running in #{path}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def slot_paths
|
|
45
|
+
Dir.glob(File.join(@worktrees.root, "slot-*")).map { |path| path.delete_suffix(".lock") }.uniq
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require_relative "git_environment"
|
|
5
|
+
|
|
6
|
+
module FunCi
|
|
7
|
+
module Pipeline
|
|
8
|
+
# The git commands a worktree slot needs, run against the project's repository.
|
|
9
|
+
class Worktrees
|
|
10
|
+
class GitError < StandardError; end
|
|
11
|
+
|
|
12
|
+
def initialize(project_root)
|
|
13
|
+
@project_root = project_root
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def root
|
|
17
|
+
File.join(File.expand_path(git(@project_root, "rev-parse", "--git-common-dir").strip, @project_root),
|
|
18
|
+
"fun-ci", "worktrees")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Leaves +path+ as a clean detached checkout of +sha+. Ignored files stay,
|
|
22
|
+
# so caches such as vendor/bundle survive between runs in the same slot.
|
|
23
|
+
def check_out(path, sha)
|
|
24
|
+
return add(path, sha) unless File.exist?(File.join(path, ".git"))
|
|
25
|
+
|
|
26
|
+
git(path, "checkout", "--detach", "--force", sha)
|
|
27
|
+
git(path, "clean", "-fd")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Has git forget the worktrees whose directories are gone.
|
|
31
|
+
def prune = git(@project_root, "worktree", "prune")
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def add(path, sha) = git(@project_root, "worktree", "add", "--detach", "--force", path, sha)
|
|
36
|
+
|
|
37
|
+
def git(dir, *args)
|
|
38
|
+
output, status = Open3.capture2e(GitEnvironment::CLEAN, "git", *args, chdir: dir)
|
|
39
|
+
raise GitError, "git #{args.join(" ")}: #{output}" unless status.success?
|
|
40
|
+
|
|
41
|
+
output
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -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
|
|
@@ -1,39 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require_relative "hook_script"
|
|
4
5
|
|
|
5
6
|
module FunCi
|
|
6
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.
|
|
7
10
|
class HookWriter
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
11
|
+
def self.run(hooks_dir:, hook_type:, stdout: $stdout)
|
|
12
|
+
new(hooks_dir: hooks_dir, hook_type: hook_type, stdout: stdout).run
|
|
26
13
|
end
|
|
27
14
|
|
|
28
|
-
def initialize(
|
|
29
|
-
@
|
|
15
|
+
def initialize(hooks_dir:, hook_type:, stdout:)
|
|
16
|
+
@hooks_dir = hooks_dir
|
|
30
17
|
@hook_type = hook_type
|
|
31
18
|
@stdout = stdout
|
|
32
19
|
end
|
|
33
20
|
|
|
34
21
|
def run
|
|
35
22
|
return reject("Not a git repository: no .git/ found.") unless git_repo?
|
|
36
|
-
return reject("Unknown hook type: #{@hook_type}") unless
|
|
23
|
+
return reject("Unknown hook type: #{@hook_type}") unless HookScript.types.include?(@hook_type)
|
|
37
24
|
return skip("Hook #{@hook_type} already exists from another tool, so it was left alone.") if foreign_hook?
|
|
38
25
|
|
|
39
26
|
write_hook
|
|
@@ -43,21 +30,16 @@ module FunCi
|
|
|
43
30
|
|
|
44
31
|
private
|
|
45
32
|
|
|
46
|
-
def git_repo?
|
|
47
|
-
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def hook_path
|
|
51
|
-
File.join(@project_root, ".git", "hooks", @hook_type)
|
|
52
|
-
end
|
|
33
|
+
def git_repo? = !@hooks_dir.nil?
|
|
34
|
+
def hook_path = File.join(@hooks_dir, @hook_type)
|
|
53
35
|
|
|
54
36
|
def foreign_hook?
|
|
55
|
-
File.exist?(hook_path) && !File.read(hook_path)
|
|
37
|
+
File.exist?(hook_path) && !HookScript.managed?(File.read(hook_path))
|
|
56
38
|
end
|
|
57
39
|
|
|
58
40
|
def write_hook
|
|
59
|
-
FileUtils.mkdir_p(
|
|
60
|
-
File.write(hook_path,
|
|
41
|
+
FileUtils.mkdir_p(@hooks_dir)
|
|
42
|
+
File.write(hook_path, HookScript.for(@hook_type))
|
|
61
43
|
File.chmod(0o755, hook_path)
|
|
62
44
|
end
|
|
63
45
|
|
|
@@ -3,51 +3,67 @@
|
|
|
3
3
|
require_relative "project_detector"
|
|
4
4
|
require_relative "template_writer"
|
|
5
5
|
require_relative "maven_linter_detector"
|
|
6
|
+
require_relative "agent_instructions"
|
|
6
7
|
|
|
7
8
|
module FunCi
|
|
8
9
|
module Setup
|
|
9
10
|
class Installer
|
|
10
|
-
def self.run(project_root:, stdout: $stdout
|
|
11
|
-
new(project_root: project_root, stdout: stdout
|
|
11
|
+
def self.run(project_root:, stdout: $stdout)
|
|
12
|
+
new(project_root: project_root, stdout: stdout).run
|
|
12
13
|
end
|
|
13
14
|
|
|
14
|
-
def initialize(project_root:, stdout
|
|
15
|
+
def initialize(project_root:, stdout:)
|
|
15
16
|
@project_root = project_root
|
|
16
17
|
@stdout = stdout
|
|
17
|
-
@pom_reader = pom_reader || ->(path) { File.read(path) }
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
# A project set up for fun-ci, new or not, also tells agents what to do.
|
|
20
21
|
def run
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
code = set_up
|
|
23
|
+
tell_agents if code.zero?
|
|
24
|
+
code
|
|
25
|
+
end
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
detected = ProjectDetector.new(filenames).detect
|
|
27
|
+
private
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return 1
|
|
32
|
-
end
|
|
29
|
+
def set_up
|
|
30
|
+
return report(".fun-ci/ already exists, so init left it as it is.", 0) if initialised?
|
|
33
31
|
|
|
34
|
-
|
|
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
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
File.write(path, merged)
|
|
45
|
+
@stdout.puts "Told agents what to do after a commit, in #{file}."
|
|
41
46
|
end
|
|
42
47
|
|
|
43
|
-
|
|
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
|
|
44
62
|
|
|
45
63
|
def detect_maven_linter(detected)
|
|
46
64
|
return nil unless detected == :jvm_maven
|
|
47
65
|
|
|
48
|
-
|
|
49
|
-
pom_content = @pom_reader.call(pom_path)
|
|
50
|
-
command = MavenLinterDetector.new(pom_content).lint_command
|
|
66
|
+
command = MavenLinterDetector.new(File.read(File.join(@project_root, "pom.xml"))).lint_command
|
|
51
67
|
command == MavenLinterDetector::DEFAULT_COMMAND ? nil : command
|
|
52
68
|
end
|
|
53
69
|
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
|