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,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../persistence/project_runs"
|
|
4
|
+
require_relative "../persistence/stage_job"
|
|
5
|
+
require_relative "run_report"
|
|
6
|
+
require_relative "events"
|
|
7
|
+
|
|
8
|
+
module FunCi
|
|
9
|
+
module Agent
|
|
10
|
+
# Looks at the newest runs of the project git names, as `events` compares
|
|
11
|
+
# them: { run id => Events::RunState }.
|
|
12
|
+
class Snapshots
|
|
13
|
+
def initialize(db, git)
|
|
14
|
+
@db = db
|
|
15
|
+
@runs = Persistence::ProjectRuns.new(db, git.toplevel)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def take(limit:)
|
|
19
|
+
@runs.recent(limit: limit, branch: nil).to_h { |run| [run[:id], state_of(run)] }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def state_of(run)
|
|
25
|
+
superseded_by = run[:status] == "cancelled" ? @runs.superseded_by(run) : nil
|
|
26
|
+
Events::RunState.new(id: run[:id], sha: run[:commit_hash], branch: run[:branch], status: run[:status],
|
|
27
|
+
finished: finished(run), superseded_by: superseded_by)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def finished(run)
|
|
31
|
+
jobs = Persistence::StageJob.for_run(@db, run[:id]).select { |job| job[:finished_order] }
|
|
32
|
+
stages = RunReport.stages(jobs).to_h { |stage| [stage.name, stage] }
|
|
33
|
+
jobs.sort_by { |job| job[:finished_order] }.map { |job| facts(stages.fetch(job[:stage])) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def facts(stage) = { stage: stage.name, state: stage.state, seconds: stage.seconds }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Agent
|
|
5
|
+
# One line saying how a stage ended: its state, its exit status or the
|
|
6
|
+
# signal that ended it, how long it took and its budget, as far as known.
|
|
7
|
+
module StageSummary
|
|
8
|
+
WORDS = { "over_budget" => "ran over budget" }.freeze
|
|
9
|
+
|
|
10
|
+
def self.line(stage)
|
|
11
|
+
"#{stage.name} #{WORDS.fetch(stage.state, stage.state)}#{ending(stage)}#{took(stage)}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.ending(stage)
|
|
15
|
+
return " (exit #{stage.exit_status})" if stage.exit_status
|
|
16
|
+
return " (killed by SIG#{stage.signal})" if stage.signal
|
|
17
|
+
|
|
18
|
+
""
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.took(stage)
|
|
22
|
+
return "" unless stage.seconds
|
|
23
|
+
|
|
24
|
+
budget = stage.budget ? ", budget #{stage.budget}s" : ""
|
|
25
|
+
" after #{stage.seconds}s#{budget}"
|
|
26
|
+
end
|
|
27
|
+
private_class_method :ending, :took
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "command_support"
|
|
4
|
+
require_relative "output"
|
|
5
|
+
|
|
6
|
+
module FunCi
|
|
7
|
+
module Agent
|
|
8
|
+
# `fun-ci status [REV] [--need LEVEL] [--json]` (acceptance-tests.md, AT-9.2).
|
|
9
|
+
class StatusCommand
|
|
10
|
+
include CommandSupport
|
|
11
|
+
|
|
12
|
+
NAME = "status"
|
|
13
|
+
|
|
14
|
+
def initialize(context)
|
|
15
|
+
@context = context
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run(args)
|
|
19
|
+
options = Options.parse(args, takes: %i[need json])
|
|
20
|
+
sha = resolve(options.rev)
|
|
21
|
+
answer(sha, reports.for(sha, options.need), Output.new(@context.io.stdout, json: options.json))
|
|
22
|
+
rescue Options::Invalid => e
|
|
23
|
+
usage(e.message)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def answer(sha, report, output)
|
|
29
|
+
return output.unknown(sha) unless report
|
|
30
|
+
|
|
31
|
+
output.report(report)
|
|
32
|
+
ExitCode::FOR.fetch(report.verdict)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "run_report"
|
|
4
|
+
|
|
5
|
+
module FunCi
|
|
6
|
+
module Agent
|
|
7
|
+
# A run report as the JSON document of acceptance-tests.md, AT-9.3.
|
|
8
|
+
# Fields once published keep their names; a change bumps SCHEMA.
|
|
9
|
+
module StatusJson
|
|
10
|
+
SCHEMA = 1
|
|
11
|
+
|
|
12
|
+
def self.document(report)
|
|
13
|
+
{ schema: SCHEMA, commit: { sha: report.sha, branch: report.branch, subject: report.subject },
|
|
14
|
+
need: report.need, verdict: report.verdict.to_s, stages: report.stages.map { |stage| stage(stage) },
|
|
15
|
+
superseded_by: report.superseded_by }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.unknown(sha) = { schema: SCHEMA, commit: { sha: sha }, verdict: "unknown" }
|
|
19
|
+
|
|
20
|
+
def self.stage(stage)
|
|
21
|
+
facts = { name: stage.name, state: stage.state, seconds: stage.seconds }
|
|
22
|
+
stage.failures.any? ? facts.merge(failures: stage.failures) : facts
|
|
23
|
+
end
|
|
24
|
+
private_class_method :stage
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "run_report"
|
|
4
|
+
require_relative "verdict"
|
|
5
|
+
require_relative "digest"
|
|
6
|
+
|
|
7
|
+
module FunCi
|
|
8
|
+
module Agent
|
|
9
|
+
# A run report as text: the commit, a line per stage with what went wrong
|
|
10
|
+
# in capitals, then the evidence of each needed stage that failed.
|
|
11
|
+
module StatusText
|
|
12
|
+
WORDS = { "failed" => "FAILED", "over_budget" => "OVER BUDGET" }.freeze
|
|
13
|
+
|
|
14
|
+
def self.lines(report)
|
|
15
|
+
needed = Verdict::LEVELS.fetch(report.need)
|
|
16
|
+
[header(report), *report.stages.map { |stage| stage_line(stage, needed) }, *Digest.lines(report.stages, needed),
|
|
17
|
+
*footer(report)]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.header(report) = %(fun-ci: #{report.sha[0, 7]} "#{report.subject}" on #{report.branch})
|
|
21
|
+
|
|
22
|
+
def self.stage_line(stage, needed)
|
|
23
|
+
seconds = stage.seconds ? format("%6.1fs", stage.seconds) : " " * 7
|
|
24
|
+
note = needed.include?(stage.name) ? "" : " (not needed)"
|
|
25
|
+
" #{stage.name.ljust(6)} #{WORDS.fetch(stage.state, stage.state).ljust(12)}#{seconds}#{note}".rstrip
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.footer(report)
|
|
29
|
+
return ["fun-ci why #{report.sha[0, 7]} #{report.deciding}"] if report.deciding
|
|
30
|
+
return [] unless report.verdict == :superseded && report.superseded_by
|
|
31
|
+
|
|
32
|
+
["Superseded by #{report.superseded_by[0, 7]}."]
|
|
33
|
+
end
|
|
34
|
+
private_class_method :stage_line, :footer
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Agent
|
|
5
|
+
# What a run means for the level an agent needs (acceptance-tests.md,
|
|
6
|
+
# §9): the stages it needs decide, in the order they finished, and a
|
|
7
|
+
# cancelled run is superseded only while they haven't.
|
|
8
|
+
module Verdict
|
|
9
|
+
LEVELS = { "build" => %w[lint build], "fast" => %w[lint build fast], "all" => %w[lint build fast slow] }.freeze
|
|
10
|
+
FINISHED_BADLY = { "failed" => :failed, "timed_out" => :over_budget }.freeze
|
|
11
|
+
|
|
12
|
+
def self.decide(run_status:, stages:, need:)
|
|
13
|
+
needed = needed(stages, need)
|
|
14
|
+
first_bad = first_finished_badly(needed)
|
|
15
|
+
return FINISHED_BADLY.fetch(first_bad[:status]) if first_bad
|
|
16
|
+
return :passed if needed.count { |stage| stage[:status] == "completed" } == LEVELS.fetch(need).size
|
|
17
|
+
|
|
18
|
+
run_status == "cancelled" ? :superseded : :undecided
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The name of the stage whose failure or overrun decided the verdict, or nil.
|
|
22
|
+
def self.deciding_stage(stages:, need:) = first_finished_badly(needed(stages, need))&.fetch(:stage)
|
|
23
|
+
|
|
24
|
+
def self.needed(stages, need) = stages.select { |stage| LEVELS.fetch(need).include?(stage[:stage]) }
|
|
25
|
+
|
|
26
|
+
def self.first_finished_badly(stages)
|
|
27
|
+
stages.select { |stage| FINISHED_BADLY.key?(stage[:status]) }.min_by { |stage| stage[:finished_order] }
|
|
28
|
+
end
|
|
29
|
+
private_class_method :needed, :first_finished_badly
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "command_support"
|
|
4
|
+
require_relative "output"
|
|
5
|
+
require_relative "waiting"
|
|
6
|
+
|
|
7
|
+
module FunCi
|
|
8
|
+
module Agent
|
|
9
|
+
# `fun-ci wait [REV] [--need LEVEL] [--within DURATION] [--follow-branch] [--json]`
|
|
10
|
+
# (acceptance-tests.md, AT-9.9 to AT-9.12).
|
|
11
|
+
class WaitCommand
|
|
12
|
+
include CommandSupport
|
|
13
|
+
|
|
14
|
+
NAME = "wait"
|
|
15
|
+
# How long a commit may go without a run before `wait` starts one; the
|
|
16
|
+
# post-commit hook's run for a commit just made turns up well within it.
|
|
17
|
+
GRACE_SECONDS = 5
|
|
18
|
+
# The answer when no run turned up and the project can't start one.
|
|
19
|
+
NOT_SET_UP = Data.define(:verdict).new(verdict: :unknown)
|
|
20
|
+
|
|
21
|
+
def initialize(context)
|
|
22
|
+
@context = context
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def run(args)
|
|
26
|
+
options = Options.parse(args, takes: %i[need json within follow_branch])
|
|
27
|
+
sha = resolve(options.rev)
|
|
28
|
+
output = Output.new(@context.io.stdout, json: options.json)
|
|
29
|
+
answer(sha, wait_for(sha, options, deadline(options), output), output)
|
|
30
|
+
rescue Options::Invalid => e
|
|
31
|
+
usage(e.message)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def deadline(options) = options.within && (@context.clock.now + options.within)
|
|
37
|
+
|
|
38
|
+
# Follows a superseded run on to the commit that superseded it, when asked to.
|
|
39
|
+
def wait_for(sha, options, deadline, output)
|
|
40
|
+
report = Waiting.new(@context.clock, deadline: deadline) { poll(sha, options.need) }.until_decided
|
|
41
|
+
return report unless options.follow_branch && report&.superseded_by
|
|
42
|
+
|
|
43
|
+
output.following(sha, report)
|
|
44
|
+
wait_for(report.superseded_by, options, deadline, output)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def answer(sha, report, output)
|
|
48
|
+
return output.no_run_yet(sha) unless report
|
|
49
|
+
return output.not_set_up(sha) if report.equal?(NOT_SET_UP)
|
|
50
|
+
|
|
51
|
+
output.report(report)
|
|
52
|
+
ExitCode::FOR.fetch(report.verdict)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def poll(sha, need)
|
|
56
|
+
@context.pipeline.watch(@context.db)
|
|
57
|
+
reports.mark_waited(sha, @context.clock.now)
|
|
58
|
+
reports.for(sha, need) || start_after_grace(sha)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Nil while waiting for a run; NOT_SET_UP once one can't be started.
|
|
62
|
+
def start_after_grace(sha)
|
|
63
|
+
@missing_since ||= @context.clock.now
|
|
64
|
+
return if @started || @context.clock.now - @missing_since < GRACE_SECONDS
|
|
65
|
+
|
|
66
|
+
@started = true
|
|
67
|
+
@context.pipeline.start(sha, @context.git.branch) ? nil : NOT_SET_UP
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Agent
|
|
5
|
+
# Polls until the answer is decided, or the deadline (a Time; nil for none) has passed
|
|
6
|
+
# (acceptance-tests.md, AT-9.9, AT-9.10). An answer is decided once its
|
|
7
|
+
# verdict is anything but undecided; nil (no run yet) is undecided.
|
|
8
|
+
class Waiting
|
|
9
|
+
POLL_SECONDS = 1
|
|
10
|
+
|
|
11
|
+
def initialize(clock, deadline:, &poll)
|
|
12
|
+
@clock = clock
|
|
13
|
+
@deadline = deadline
|
|
14
|
+
@poll = poll
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def until_decided
|
|
18
|
+
loop do
|
|
19
|
+
answer = @poll.call
|
|
20
|
+
return answer if decided?(answer) || out_of_time?
|
|
21
|
+
|
|
22
|
+
@clock.pause(POLL_SECONDS)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def decided?(answer) = answer && answer.verdict != :undecided
|
|
29
|
+
def out_of_time? = @deadline && @clock.now >= @deadline
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "command_support"
|
|
4
|
+
require_relative "output"
|
|
5
|
+
|
|
6
|
+
module FunCi
|
|
7
|
+
module Agent
|
|
8
|
+
# `fun-ci why [REV] [STAGE] [--need LEVEL] [--json] [--raw]`
|
|
9
|
+
# (acceptance-tests.md, AT-10.1, AT-10.6): everything kept about the stage
|
|
10
|
+
# named, or else about the stage that decided the verdict; with --raw, the
|
|
11
|
+
# raw output it kept, and nothing else.
|
|
12
|
+
class WhyCommand
|
|
13
|
+
include CommandSupport
|
|
14
|
+
|
|
15
|
+
NAME = "why"
|
|
16
|
+
|
|
17
|
+
def initialize(context)
|
|
18
|
+
@context = context
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run(args)
|
|
22
|
+
options = Options.parse(args, takes: %i[need json stage raw])
|
|
23
|
+
sha = resolve(options.rev)
|
|
24
|
+
answer(sha, reports.for(sha, options.need), options)
|
|
25
|
+
rescue Options::Invalid => e
|
|
26
|
+
usage(e.message)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def answer(sha, report, options)
|
|
32
|
+
output = Output.new(@context.io.stdout, json: options.json)
|
|
33
|
+
return output.unknown(sha) unless report
|
|
34
|
+
|
|
35
|
+
stage_name = options.stage || report.deciding
|
|
36
|
+
options.raw ? raw(report, stage_name) : output.why(report, stage_name)
|
|
37
|
+
ExitCode::FOR.fetch(report.verdict)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def raw(report, stage_name)
|
|
41
|
+
stage = report.stages.find { |candidate| candidate.name == stage_name }
|
|
42
|
+
text = stage && reports.raw_output(stage)
|
|
43
|
+
return @context.io.stdout.write(text) if text
|
|
44
|
+
|
|
45
|
+
@context.io.stderr.puts "fun-ci why: no raw output is kept for #{stage_name || "any stage"} of #{report.sha[0,
|
|
46
|
+
7]}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "run_report"
|
|
4
|
+
|
|
5
|
+
module FunCi
|
|
6
|
+
module Agent
|
|
7
|
+
# `fun-ci why --json` (acceptance-tests.md, AT-10.2): the stage and how it
|
|
8
|
+
# ended, its evidence, or else why there is none. Fields once published
|
|
9
|
+
# keep their names; a change bumps SCHEMA.
|
|
10
|
+
module WhyJson
|
|
11
|
+
SCHEMA = 1
|
|
12
|
+
FAILED = %w[failed over_budget].freeze
|
|
13
|
+
|
|
14
|
+
def self.document(report, stage)
|
|
15
|
+
reason = no_evidence(report, stage)
|
|
16
|
+
{ schema: SCHEMA, commit: { sha: report.sha, branch: report.branch, subject: report.subject },
|
|
17
|
+
**ending(stage), evidence: reason ? nil : stage.evidence.to_h, no_evidence: reason,
|
|
18
|
+
raw_output: { kept: !stage&.raw_bytes.nil?, bytes: stage&.raw_bytes || 0 } }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.ending(stage)
|
|
22
|
+
{ stage: stage&.name, state: stage&.state, exit_status: stage&.exit_status, signal: stage&.signal,
|
|
23
|
+
seconds: stage&.seconds, budget: stage&.budget }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.no_evidence(report, stage)
|
|
27
|
+
return report.verdict == :passed ? "passed" : "running" unless stage
|
|
28
|
+
return stage.state unless FAILED.include?(stage.state)
|
|
29
|
+
|
|
30
|
+
stage.pruned? ? "pruned" : nil
|
|
31
|
+
end
|
|
32
|
+
private_class_method :ending, :no_evidence
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "status_text"
|
|
4
|
+
require_relative "stage_summary"
|
|
5
|
+
require_relative "evidence_text"
|
|
6
|
+
require_relative "../persistence/pipeline_recorder"
|
|
7
|
+
|
|
8
|
+
module FunCi
|
|
9
|
+
module Agent
|
|
10
|
+
# What `fun-ci why` prints about one stage (acceptance-tests.md, AT-10.1):
|
|
11
|
+
# the commit, how the stage ended, then everything kept about it.
|
|
12
|
+
module WhyText
|
|
13
|
+
PASSED = "Nothing is kept about a stage that passed."
|
|
14
|
+
PRUNED = "Its evidence is no longer kept: fun-ci keeps it for a project's " \
|
|
15
|
+
"#{Persistence::DbRecorder::KEPT_RUNS} newest runs.".freeze
|
|
16
|
+
|
|
17
|
+
ONLY_TAIL = "Only the output's last lines were kept; extractors under `evidence:` in .fun-ci/config keep more."
|
|
18
|
+
|
|
19
|
+
def self.lines(report, stage)
|
|
20
|
+
[StatusText.header(report), StageSummary.line(stage), *body(stage), *raw(report, stage), *only_tail(stage)]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# So an agent that reads it knows it can write the extractor itself.
|
|
24
|
+
def self.only_tail(stage)
|
|
25
|
+
evidence = stage.evidence
|
|
26
|
+
kept_more = evidence.failures.any? || evidence.excerpts.any? { |excerpt| excerpt[:extractor] != "output-tail" }
|
|
27
|
+
kept_more || evidence.excerpts.empty? ? [] : ["", ONLY_TAIL]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.raw(report, stage)
|
|
31
|
+
stage.raw_bytes ? ["", "The whole output: fun-ci why #{report.sha[0, 7]} #{stage.name} --raw"] : []
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.body(stage)
|
|
35
|
+
return [PASSED] if stage.state == "passed"
|
|
36
|
+
return [PRUNED] if stage.pruned?
|
|
37
|
+
|
|
38
|
+
EvidenceText.lines(stage.evidence)
|
|
39
|
+
end
|
|
40
|
+
private_class_method :body, :raw, :only_tail
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/fun_ci/cli.rb
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "tmpdir"
|
|
4
3
|
require_relative "../fun_ci"
|
|
4
|
+
require_relative "cli_help"
|
|
5
|
+
require_relative "pipeline/trigger_params"
|
|
6
|
+
require_relative "persistence/state_dir"
|
|
7
|
+
require_relative "setup/commands"
|
|
5
8
|
|
|
6
9
|
module FunCi
|
|
7
10
|
class Cli
|
|
@@ -10,27 +13,31 @@ module FunCi
|
|
|
10
13
|
"console" => :run_console,
|
|
11
14
|
"init" => :run_init,
|
|
12
15
|
"install-hooks" => :run_install_hooks,
|
|
13
|
-
"check" => :run_check
|
|
16
|
+
"check" => :run_check,
|
|
17
|
+
"prune" => :run_prune,
|
|
18
|
+
"extract" => :run_extract
|
|
14
19
|
}.freeze
|
|
20
|
+
AGENT_COMMANDS = %w[status runs wait events why].freeze
|
|
15
21
|
|
|
16
|
-
def self.
|
|
17
|
-
|
|
22
|
+
def self.default_db_dir = Persistence::StateDir.path(ENV)
|
|
23
|
+
|
|
24
|
+
def self.run(args, io: Pipeline::Io.new, handlers: {}, db_dir: default_db_dir)
|
|
25
|
+
new(io: io, handlers: handlers, db_dir: db_dir).run(args)
|
|
18
26
|
end
|
|
19
27
|
|
|
20
|
-
def initialize(
|
|
21
|
-
@
|
|
22
|
-
@
|
|
28
|
+
def initialize(io:, handlers:, db_dir:)
|
|
29
|
+
@io = io
|
|
30
|
+
@db_dir = db_dir
|
|
23
31
|
@handlers = handlers
|
|
24
32
|
end
|
|
25
33
|
|
|
26
34
|
def run(args)
|
|
27
35
|
subcommand = args.first
|
|
28
|
-
return help(
|
|
29
|
-
return version if subcommand == "--version"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
end
|
|
36
|
+
return CliHelp.help(@io.stdout) if %w[-h --help].include?(subcommand)
|
|
37
|
+
return CliHelp.version(@io.stdout) if subcommand == "--version"
|
|
38
|
+
|
|
39
|
+
return unknown_command(subcommand) unless ROUTES.key?(subcommand) || AGENT_COMMANDS.include?(subcommand)
|
|
40
|
+
|
|
34
41
|
dispatch(subcommand, args.drop(1))
|
|
35
42
|
end
|
|
36
43
|
|
|
@@ -38,6 +45,8 @@ module FunCi
|
|
|
38
45
|
|
|
39
46
|
def dispatch(subcommand, args)
|
|
40
47
|
return @handlers[subcommand].call(args) if @handlers.key?(subcommand)
|
|
48
|
+
return run_agent(subcommand, args) if AGENT_COMMANDS.include?(subcommand)
|
|
49
|
+
|
|
41
50
|
send(ROUTES[subcommand], args)
|
|
42
51
|
end
|
|
43
52
|
|
|
@@ -46,105 +55,74 @@ module FunCi
|
|
|
46
55
|
require_relative "persistence/pipeline_recorder"
|
|
47
56
|
db = setup_db
|
|
48
57
|
recorder = Persistence::DbRecorder.new(db)
|
|
49
|
-
Pipeline::Trigger.run_from_args(args,
|
|
58
|
+
Pipeline::Trigger.run_from_args(args, io: @io, recorder: recorder)
|
|
50
59
|
end
|
|
51
60
|
|
|
61
|
+
# Only `console` needs the renderer, so only it fails without one.
|
|
52
62
|
def run_console(_args)
|
|
53
|
-
require_relative "
|
|
54
|
-
require_relative "
|
|
55
|
-
|
|
63
|
+
require_relative "console/launcher"
|
|
64
|
+
require_relative "console/renderer_lookup"
|
|
65
|
+
launch_console(Console::RendererLookup.default.path)
|
|
66
|
+
rescue Console::RendererLookup::Missing => e
|
|
67
|
+
@io.stderr.puts e.message
|
|
68
|
+
1
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def launch_console(renderer)
|
|
56
72
|
db = setup_db
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
height_provider: -> { IO.console&.winsize&.dig(0) },
|
|
61
|
-
animation_renderer: Tui::AnimationRenderer.new
|
|
62
|
-
)
|
|
63
|
-
tui.run
|
|
64
|
-
0
|
|
73
|
+
Console::Launcher.run(renderer: renderer, db: db, project_dir: Dir.pwd, stderr: @io.stderr)
|
|
74
|
+
ensure
|
|
75
|
+
db&.close
|
|
65
76
|
end
|
|
66
77
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
# The mutation lane loads all of lib/ before it mutates, so it can't see
|
|
79
|
+
# a lazy require go missing (here and in run_prune); the process tests
|
|
80
|
+
# that run these commands load the files unmutated.
|
|
81
|
+
def run_agent(name, args)
|
|
82
|
+
require_relative "agent/commands" # mutineer:disable-line statement_removal
|
|
83
|
+
db = setup_db
|
|
84
|
+
Agent::Commands.run(name, args, agent_context(db))
|
|
85
|
+
ensure
|
|
86
|
+
db&.close
|
|
74
87
|
end
|
|
75
88
|
|
|
76
|
-
def
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
def agent_context(db)
|
|
90
|
+
Agent::Context.new(db: db, git: Agent::Git.new(Dir.pwd), io: @io, clock: Agent::SystemClock.new,
|
|
91
|
+
pipeline: Agent::LivePipeline.new(Dir.pwd))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def run_init(args) = setup_commands.init(args)
|
|
95
|
+
def run_install_hooks(args) = setup_commands.install_hooks(args)
|
|
96
|
+
def run_check(args) = setup_commands.check(args)
|
|
97
|
+
def setup_commands = Setup::Commands.new(Dir.pwd, @io.stdout)
|
|
98
|
+
|
|
99
|
+
def run_extract(args)
|
|
100
|
+
require_relative "evidence/extract_command"
|
|
101
|
+
Evidence::ExtractCommand.new(Dir.pwd, @io).run(args)
|
|
84
102
|
end
|
|
85
103
|
|
|
86
|
-
def
|
|
87
|
-
require_relative "
|
|
88
|
-
|
|
104
|
+
def run_prune(_args)
|
|
105
|
+
require_relative "pipeline/worktree_prune" # mutineer:disable-line statement_removal
|
|
106
|
+
removed = Pipeline::WorktreePrune.new(Pipeline::Worktrees.new(Dir.pwd)).run
|
|
107
|
+
@io.stdout.puts "Removed #{removed} fun-ci worktree#{"s" unless removed == 1}."
|
|
108
|
+
0
|
|
109
|
+
rescue Pipeline::WorktreePrune::Busy, Pipeline::Worktrees::GitError => e
|
|
110
|
+
@io.stderr.puts "fun-ci prune: #{e.message}"
|
|
111
|
+
1
|
|
89
112
|
end
|
|
90
113
|
|
|
91
114
|
def setup_db
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
db_path = File.join(db_dir, "db.sqlite3")
|
|
115
|
+
Persistence::StateDir.prepare(@db_dir)
|
|
116
|
+
db_path = File.join(@db_dir, "db.sqlite3")
|
|
95
117
|
db = Persistence::Database.connection(db_path)
|
|
96
118
|
Persistence::Database.migrate!(db)
|
|
97
119
|
db
|
|
98
120
|
end
|
|
99
121
|
|
|
100
|
-
|
|
101
|
-
fun-ci
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
fun-ci <command> [options]
|
|
105
|
-
|
|
106
|
-
Commands:
|
|
107
|
-
trigger Run CI pipeline for a commit
|
|
108
|
-
console Launch the admin TUI dashboard
|
|
109
|
-
init Initialize .fun-ci/ with template scripts
|
|
110
|
-
install-hooks Install pre-commit and pre-push git hooks
|
|
111
|
-
check Verify project setup
|
|
112
|
-
|
|
113
|
-
Options:
|
|
114
|
-
-h, --help Show this help message
|
|
115
|
-
--version Show version
|
|
116
|
-
|
|
117
|
-
Trigger options:
|
|
118
|
-
--no-validate Fork pipeline to background and return immediately
|
|
119
|
-
|
|
120
|
-
Init options:
|
|
121
|
-
--everything Run init + install-hooks + check in one step
|
|
122
|
-
|
|
123
|
-
Examples:
|
|
124
|
-
fun-ci init --everything
|
|
125
|
-
fun-ci trigger abc1234 main
|
|
126
|
-
fun-ci trigger --no-validate abc1234 main
|
|
127
|
-
fun-ci console
|
|
128
|
-
HELP
|
|
129
|
-
|
|
130
|
-
def help(exit_code)
|
|
131
|
-
@stdout.puts HELP_TEXT
|
|
132
|
-
exit_code
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def version
|
|
136
|
-
@stdout.puts "fun-ci #{FunCi::VERSION}"
|
|
137
|
-
0
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def print_error(subcommand)
|
|
141
|
-
if subcommand
|
|
142
|
-
@stderr.puts "fun-ci: unknown command '#{subcommand}'"
|
|
143
|
-
@stderr.puts ""
|
|
144
|
-
end
|
|
145
|
-
@stderr.puts "Usage: fun-ci <command> [options]"
|
|
146
|
-
@stderr.puts ""
|
|
147
|
-
@stderr.puts "Run 'fun-ci --help' for available commands."
|
|
122
|
+
def unknown_command(subcommand)
|
|
123
|
+
@io.stderr.puts "fun-ci: unknown command '#{subcommand}'", "" if subcommand
|
|
124
|
+
@io.stderr.puts "Usage: fun-ci <command> [options]", "", "Run 'fun-ci --help' for available commands."
|
|
125
|
+
1
|
|
148
126
|
end
|
|
149
127
|
end
|
|
150
128
|
end
|