ruby_raider 4.1.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/e2e_tests.yml +3 -0
- data/README.md +52 -53
- data/RELEASE.md +14 -1
- data/lib/commands/schedule_commands.rb +84 -0
- data/lib/generators/templates/common/git_ignore.tt +1 -0
- data/lib/ruby_raider/runtime/retry_reset.rb +32 -0
- data/lib/ruby_raider/runtime/rspec.rb +1 -0
- data/lib/ruby_raider/runtime/show_browser.rb +53 -0
- data/lib/ruby_raider/runtime.rb +2 -0
- data/lib/ruby_raider.rb +22 -7
- data/lib/utilities/schedules/adapter.rb +22 -0
- data/lib/utilities/schedules/base.rb +27 -0
- data/lib/utilities/schedules/kind.rb +74 -0
- data/lib/utilities/schedules/legacy.rb +196 -0
- data/lib/utilities/schedules/legacy_stub.rb +62 -0
- data/lib/utilities/{flows → schedules}/next_run.rb +2 -2
- data/lib/utilities/{flows → schedules}/notifier.rb +6 -6
- data/lib/utilities/{flows → schedules}/os/command_runner.rb +1 -1
- data/lib/utilities/{flows → schedules}/os/cron.rb +27 -17
- data/lib/utilities/{flows → schedules}/os/launchd.rb +32 -14
- data/lib/utilities/{flows → schedules}/os/linux.rb +23 -5
- data/lib/utilities/{flows → schedules}/os/systemd.rb +33 -17
- data/lib/utilities/{flows → schedules}/os/task_scheduler.rb +36 -20
- data/lib/utilities/schedules/os_enumerator.rb +30 -0
- data/lib/utilities/schedules/project.rb +70 -0
- data/lib/utilities/schedules/rule.rb +162 -0
- data/lib/utilities/schedules/run_lock.rb +160 -0
- data/lib/utilities/schedules/run_log.rb +122 -0
- data/lib/utilities/schedules/run_report.rb +139 -0
- data/lib/utilities/schedules/run_wrapper.rb +216 -0
- data/lib/utilities/schedules/schedule.rb +89 -0
- data/lib/utilities/schedules/schedule_store.rb +53 -0
- data/lib/utilities/schedules/scheduler.rb +178 -0
- data/lib/utilities/schedules/scheduler_steps.rb +69 -0
- data/lib/utilities/schedules/selection.rb +133 -0
- data/lib/utilities/schedules/spawn.rb +68 -0
- data/lib/utilities/{flows/schedule_store.rb → schedules/store.rb} +17 -13
- data/lib/version +1 -1
- data/ruby_raider.gemspec +1 -0
- data/sig/commands/schedule_commands.rbs +12 -0
- data/sig/ruby_raider/runtime/retry_reset.rbs +7 -0
- data/sig/ruby_raider/runtime/show_browser.rbs +27 -0
- data/sig/ruby_raider.rbs +4 -0
- data/sig/utilities/schedules/adapter.rbs +5 -0
- data/sig/utilities/schedules/base.rbs +15 -0
- data/sig/utilities/schedules/kind.rbs +32 -0
- data/sig/utilities/schedules/legacy.rbs +45 -0
- data/sig/utilities/schedules/legacy_stub.rbs +15 -0
- data/sig/utilities/{flows → schedules}/next_run.rbs +3 -3
- data/sig/utilities/{flows → schedules}/notifier.rbs +1 -1
- data/sig/utilities/{flows → schedules}/os/command_runner.rbs +1 -1
- data/sig/utilities/{flows → schedules}/os/cron.rbs +7 -7
- data/sig/utilities/{flows → schedules}/os/launchd.rbs +7 -6
- data/sig/utilities/{flows → schedules}/os/linux.rbs +10 -4
- data/sig/utilities/{flows → schedules}/os/systemd.rbs +8 -7
- data/sig/utilities/{flows → schedules}/os/task_scheduler.rbs +13 -10
- data/sig/utilities/schedules/os_enumerator.rbs +12 -0
- data/sig/utilities/schedules/project.rbs +22 -0
- data/sig/utilities/{flows/schedule.rbs → schedules/rule.rbs} +23 -12
- data/sig/utilities/schedules/run_lock.rbs +29 -0
- data/sig/utilities/schedules/run_log.rbs +33 -0
- data/sig/utilities/schedules/run_report.rbs +34 -0
- data/sig/utilities/schedules/run_wrapper.rbs +45 -0
- data/sig/utilities/schedules/schedule.rbs +27 -0
- data/sig/utilities/schedules/schedule_store.rbs +21 -0
- data/sig/utilities/{flows → schedules}/scheduler.rbs +22 -15
- data/sig/utilities/schedules/scheduler_steps.rbs +22 -0
- data/sig/utilities/schedules/selection.rbs +36 -0
- data/sig/utilities/schedules/spawn.rbs +10 -0
- data/sig/utilities/{flows/schedule_store.rbs → schedules/store.rbs} +14 -9
- data/spec/commands/schedule_commands_spec.rb +284 -0
- data/spec/integration/content/common_content_spec.rb +8 -2
- data/spec/integration/content/gemfile_content_spec.rb +2 -2
- data/spec/integration/end_to_end_spec.rb +0 -63
- data/spec/runtime/retry_reset_spec.rb +112 -0
- data/spec/runtime/show_browser_project_spec.rb +70 -0
- data/spec/runtime/show_browser_spec.rb +101 -0
- data/spec/utilities/schedules/kind_spec.rb +81 -0
- data/spec/utilities/schedules/legacy_spec.rb +433 -0
- data/spec/utilities/schedules/legacy_stub_spec.rb +144 -0
- data/spec/utilities/{flows → schedules}/next_run_spec.rb +5 -5
- data/spec/utilities/{flows → schedules}/notifier_spec.rb +3 -4
- data/spec/utilities/{flows → schedules}/os/command_runner_spec.rb +2 -3
- data/spec/utilities/{flows → schedules}/os/cron_spec.rb +34 -29
- data/spec/utilities/{flows → schedules}/os/launchd_spec.rb +42 -29
- data/spec/utilities/{flows → schedules}/os/linux_spec.rb +27 -13
- data/spec/utilities/schedules/os/list_ids_spec.rb +202 -0
- data/spec/utilities/{flows → schedules}/os/systemd_spec.rb +35 -31
- data/spec/utilities/{flows → schedules}/os/task_scheduler_spec.rb +40 -34
- data/spec/utilities/schedules/os_entry_spec.rb +54 -0
- data/spec/utilities/schedules/os_enumerator_spec.rb +57 -0
- data/spec/utilities/schedules/project_spec.rb +125 -0
- data/spec/utilities/schedules/run_lock_spec.rb +143 -0
- data/spec/utilities/schedules/run_log_spec.rb +83 -0
- data/spec/utilities/schedules/run_report_spec.rb +150 -0
- data/spec/utilities/schedules/run_wrapper_integration_spec.rb +166 -0
- data/spec/utilities/schedules/run_wrapper_spec.rb +430 -0
- data/spec/utilities/schedules/schedule_spec.rb +120 -0
- data/spec/utilities/schedules/schedule_store_spec.rb +151 -0
- data/spec/utilities/schedules/scheduler_launchd_spec.rb +77 -0
- data/spec/utilities/schedules/scheduler_spec.rb +445 -0
- data/spec/utilities/schedules/selection_spec.rb +176 -0
- data/spec/utilities/schedules/spawn_spec.rb +104 -0
- metadata +106 -53
- data/lib/commands/flow_commands.rb +0 -104
- data/lib/generators/templates/flows/flow_helper.tt +0 -42
- data/lib/generators/templates/flows/run.tt +0 -332
- data/lib/utilities/flows/project.rb +0 -170
- data/lib/utilities/flows/run_log.rb +0 -61
- data/lib/utilities/flows/runner.rb +0 -206
- data/lib/utilities/flows/schedule.rb +0 -221
- data/lib/utilities/flows/scheduler.rb +0 -187
- data/sig/commands/flow_commands.rbs +0 -13
- data/sig/utilities/flows/project.rbs +0 -46
- data/sig/utilities/flows/run_log.rbs +0 -22
- data/sig/utilities/flows/runner.rbs +0 -35
- data/spec/commands/flow_commands_spec.rb +0 -128
- data/spec/generators/flow_helper_spec.rb +0 -40
- data/spec/integration/flows/flow_project.rb +0 -62
- data/spec/integration/flows/run_command_spec.rb +0 -91
- data/spec/integration/flows/run_lifecycle_spec.rb +0 -383
- data/spec/utilities/flows/project_spec.rb +0 -178
- data/spec/utilities/flows/run_log_spec.rb +0 -47
- data/spec/utilities/flows/runner_spec.rb +0 -313
- data/spec/utilities/flows/schedule_spec.rb +0 -233
- data/spec/utilities/flows/schedule_store_spec.rb +0 -137
- data/spec/utilities/flows/scheduler_spec.rb +0 -329
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'English'
|
|
4
|
-
require 'securerandom'
|
|
5
|
-
require 'time'
|
|
6
|
-
require_relative 'project'
|
|
7
|
-
require_relative 'run_log'
|
|
8
|
-
require_relative 'notifier'
|
|
9
|
-
|
|
10
|
-
module RubyRaider
|
|
11
|
-
module Flows
|
|
12
|
-
# `raider flow run`: a thin wrapper around `bundle exec rspec flows/<name>_flow.rb`. The flow's own
|
|
13
|
-
# flows/support/run.rb records the run; this sets its env, retries the whole flow and maps the
|
|
14
|
-
# recorded status to the exit code.
|
|
15
|
-
# :reek:TooManyInstanceVariables { max_instance_variables: 6 } -- the project, injected collaborators and the last run line
|
|
16
|
-
class Runner
|
|
17
|
-
EXIT_CODES = { 'completed' => 0, 'failed' => 1, 'setup_error' => 2, 'busy' => 3 }.freeze
|
|
18
|
-
INPUT = /\A[a-z][a-z0-9_]{0,39}\z/
|
|
19
|
-
INPUT_RULE = 'use name=value, the name 1 to 40 of a-z, 0-9 and _ starting with a letter'
|
|
20
|
-
TRIGGERS = %w[manual schedule desktop].freeze
|
|
21
|
-
RETRY_WAIT = 30
|
|
22
|
-
|
|
23
|
-
class << self
|
|
24
|
-
# True once INT or TERM stopped the last spawned run: no retry after that
|
|
25
|
-
attr_accessor :stopped
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Runs argv with no shell, under the project's own bundle rather than the one raider runs in.
|
|
29
|
-
# The flow stays in the terminal's process group (it can read the terminal, and a closed terminal
|
|
30
|
-
# ends it). Its flows/support/run.rb turns the first INT into an Interrupt that ends the step and
|
|
31
|
-
# still records the run, and ignores the rest. nil when it could not start: no log line, so exit 2.
|
|
32
|
-
def self.spawn(env, argv, chdir)
|
|
33
|
-
self.stopped = false
|
|
34
|
-
pid = nil
|
|
35
|
-
forwarded = false
|
|
36
|
-
forward = lambda do
|
|
37
|
-
next if pid.nil? || forwarded || Gem.win_platform? # the Windows console already sent Ctrl-C to the flow
|
|
38
|
-
|
|
39
|
-
forwarded = true
|
|
40
|
-
interrupt(pid)
|
|
41
|
-
end
|
|
42
|
-
# Before spawning, so a Ctrl-C in between is not lost
|
|
43
|
-
previous = %w[INT TERM].to_h do |signal|
|
|
44
|
-
[signal, trap(signal) do
|
|
45
|
-
self.stopped = true
|
|
46
|
-
forward.call
|
|
47
|
-
end]
|
|
48
|
-
end
|
|
49
|
-
run = lambda do
|
|
50
|
-
pid = Process.spawn(env, *argv, chdir: chdir)
|
|
51
|
-
forward.call if stopped
|
|
52
|
-
Process.wait(pid)
|
|
53
|
-
$CHILD_STATUS.exitstatus
|
|
54
|
-
end
|
|
55
|
-
defined?(Bundler) ? Bundler.with_unbundled_env(&run) : run.call
|
|
56
|
-
rescue SystemCallError => e
|
|
57
|
-
warn("Could not start #{argv.first}: #{e.message}")
|
|
58
|
-
nil
|
|
59
|
-
ensure
|
|
60
|
-
previous&.each { |signal, handler| trap(signal, handler || 'DEFAULT') }
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# INT, sent once: from a terminal the flow already has its own, which it ignores as a repeat
|
|
64
|
-
def self.interrupt(pid)
|
|
65
|
-
Process.kill('INT', pid)
|
|
66
|
-
rescue SystemCallError
|
|
67
|
-
nil
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# notifier: nil makes a Notifier only when a run with notify: true needs one
|
|
71
|
-
def initialize(project, out: $stdout, spawner: Runner.method(:spawn), sleeper: ->(seconds) { sleep(seconds) }, notifier: nil)
|
|
72
|
-
@project = project
|
|
73
|
-
@out = out
|
|
74
|
-
@spawner = spawner
|
|
75
|
-
@sleeper = sleeper
|
|
76
|
-
@notifier = notifier
|
|
77
|
-
@last_line = nil
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Returns the exit code: 0 completed, 1 a step failed, 2 setup failed, 3 already running.
|
|
81
|
-
# notify: true sends one desktop notification when the run ends in neither completed nor busy.
|
|
82
|
-
def run(name, headed: false, retries: 0, inputs: [], trigger: 'manual', notify: false)
|
|
83
|
-
@project.flow_file(name)
|
|
84
|
-
raise Error, 'Flows are not set up in this project: run raider flow init' unless @project.set_up?
|
|
85
|
-
|
|
86
|
-
options = { headed: headed, inputs: input_env(inputs), trigger: checked_trigger(trigger) }
|
|
87
|
-
attempts = checked_retries(retries) + 1
|
|
88
|
-
status = attempt_runs(name, attempts, options)
|
|
89
|
-
# Not after a stop: whoever pressed Ctrl-C already knows
|
|
90
|
-
notify_failure(name, status, options[:trigger]) if notify && !Runner.stopped && !%w[completed busy].include?(status)
|
|
91
|
-
EXIT_CODES.fetch(status, 2)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
private
|
|
95
|
-
|
|
96
|
-
def attempt_runs(name, attempts, options)
|
|
97
|
-
(1..attempts).each do |attempt|
|
|
98
|
-
status = attempt_run(name, attempt, options)
|
|
99
|
-
return stopped(name, status) if Runner.stopped
|
|
100
|
-
return status if %w[completed busy].include?(status) || attempt == attempts
|
|
101
|
-
|
|
102
|
-
@out.puts("Retrying in #{RETRY_WAIT} s (attempt #{attempt + 1} of #{attempts})")
|
|
103
|
-
@sleeper.call(RETRY_WAIT)
|
|
104
|
-
end
|
|
105
|
-
'setup_error'
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# The notifier never raises, so a notification can't change the exit code
|
|
109
|
-
def notify_failure(name, status, trigger)
|
|
110
|
-
message = status == 'failed' ? "Failed at #{failed_step(@last_line&.fetch('failed_step', nil))}" : "Could not start: see #{start_output(name, trigger)}"
|
|
111
|
-
(@notifier ||= Notifier.new).notify("Ruby Raider flow #{name}", message)
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Only a scheduled run outside Windows has a scheduler.log: Windows tasks keep none
|
|
115
|
-
def start_output(name, trigger)
|
|
116
|
-
trigger == 'schedule' && !Gem.win_platform? ? ".raider/flows/#{name}/scheduler.log" : 'the run output'
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def stopped(name, status)
|
|
120
|
-
@out.puts("Flow #{name} was stopped: no retry")
|
|
121
|
-
status
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def attempt_run(name, attempt, options)
|
|
125
|
-
run_id = SecureRandom.hex(8)
|
|
126
|
-
# The literal name-built path ([a-z0-9_] only), not the realpath flow_file checked
|
|
127
|
-
argv = ['bundle', 'exec', 'rspec', "flows/#{name}_flow.rb", '--tag', 'flow']
|
|
128
|
-
code = @spawner.call(run_env(name, attempt, run_id, options), argv, @project.root)
|
|
129
|
-
line = RunLog.new(@project, name).find(run_id)
|
|
130
|
-
@last_line = line
|
|
131
|
-
report(name, line, code)
|
|
132
|
-
line ? line['status'].to_s : 'setup_error'
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Every RAIDER_FLOW_* and RAIDER_INPUT_* is set (or unset) here, so a value left in the shell never leaks into a run
|
|
136
|
-
def run_env(name, attempt, run_id, options)
|
|
137
|
-
headed = options[:headed]
|
|
138
|
-
stale_inputs = ENV.keys.grep(/\ARAIDER_INPUT_/).to_h { |key| [key, nil] }
|
|
139
|
-
stale_inputs.merge(
|
|
140
|
-
'BUNDLE_GEMFILE' => @project.path('Gemfile'),
|
|
141
|
-
'RAIDER_FLOW_NAME' => name,
|
|
142
|
-
'RAIDER_FLOW_ATTEMPT' => attempt.to_s,
|
|
143
|
-
'RAIDER_FLOW_TRIGGER' => options[:trigger],
|
|
144
|
-
'RAIDER_FLOW_RUN_ID' => run_id,
|
|
145
|
-
'RAIDER_FLOW_HEADED' => headed ? '1' : nil,
|
|
146
|
-
'HEADLESS' => headed ? nil : 'true',
|
|
147
|
-
'RETRY_COUNT' => '0'
|
|
148
|
-
).merge(options[:inputs])
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
# name=value pairs; values reach the flow only through RAIDER_INPUT_<NAME>, never argv
|
|
152
|
-
def input_env(inputs)
|
|
153
|
-
Array(inputs).to_h do |pair|
|
|
154
|
-
key, value = pair.to_s.split('=', 2)
|
|
155
|
-
raise Error, "Invalid input #{key.to_s.inspect}: #{INPUT_RULE}" unless value && INPUT.match?(key.to_s)
|
|
156
|
-
raise Error, "Invalid input #{key.inspect}: the value contains a NUL byte" if value.include?("\0")
|
|
157
|
-
|
|
158
|
-
["RAIDER_INPUT_#{key.to_s.upcase}", value]
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def checked_trigger(trigger)
|
|
163
|
-
return trigger if TRIGGERS.include?(trigger)
|
|
164
|
-
|
|
165
|
-
raise Error, "Invalid trigger #{trigger.inspect}: use #{TRIGGERS.join(', ')}"
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def checked_retries(retries)
|
|
169
|
-
return retries if retries.is_a?(Integer) && retries.between?(0, 3)
|
|
170
|
-
|
|
171
|
-
raise Error, "Invalid retries #{retries.inspect}: use 0 to 3"
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def report(name, line, code)
|
|
175
|
-
return @out.puts("Flow #{name} did not record a run (exit #{code || 'none'}): see the output above") unless line
|
|
176
|
-
|
|
177
|
-
@out.puts("Flow #{name} #{Flows.printable(outcome(line))}")
|
|
178
|
-
results = line['results_dir']
|
|
179
|
-
@out.puts("Results: #{Flows.printable(results)}") if results.is_a?(String) && File.directory?(@project.path(results))
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def outcome(line)
|
|
183
|
-
case line['status']
|
|
184
|
-
when 'completed' then "completed in #{duration(line)} s"
|
|
185
|
-
when 'failed' then "failed at #{failed_step(line['failed_step'])}"
|
|
186
|
-
when 'busy' then 'is already running'
|
|
187
|
-
else 'could not start: see the output above'
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def failed_step(step)
|
|
192
|
-
step = {} unless step.is_a?(Hash)
|
|
193
|
-
number = step['number']
|
|
194
|
-
where = number ? "step #{number}" : "line #{step['line'] || '?'}"
|
|
195
|
-
title = step['title']
|
|
196
|
-
title ? "#{where} (#{title})" : where
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def duration(line)
|
|
200
|
-
(Time.iso8601(line['finished_at'].to_s) - Time.iso8601(line['started_at'].to_s)).round(1)
|
|
201
|
-
rescue ArgumentError
|
|
202
|
-
'?'
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
end
|
|
206
|
-
end
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'digest'
|
|
4
|
-
require 'rbconfig'
|
|
5
|
-
require 'time'
|
|
6
|
-
require_relative 'project'
|
|
7
|
-
require_relative 'runner'
|
|
8
|
-
|
|
9
|
-
module RubyRaider
|
|
10
|
-
module Flows
|
|
11
|
-
# One scheduled flow: the rule (every/at/on), how it runs (retries, inputs) and what the OS entry
|
|
12
|
-
# runs (absolute ruby and bundle, GEM_HOME/GEM_PATH captured when it was scheduled). Immutable.
|
|
13
|
-
# :reek:TooManyInstanceVariables { max_instance_variables: 13 } -- the fields of a schedules.yml entry
|
|
14
|
-
# :reek:TooManyConstants { max_constants: 9 } -- the rule vocabulary, the OS entry parts and the yml keys
|
|
15
|
-
class Schedule
|
|
16
|
-
EVERY = %w[hour day weekday week].freeze
|
|
17
|
-
DAYS = %w[mon tue wed thu fri sat sun].freeze
|
|
18
|
-
DAYS_RULE = 'use mon, tue, wed, thu, fri, sat or sun'
|
|
19
|
-
CLOCK = /\A([01]\d|2[0-3]):([0-5]\d)\z/
|
|
20
|
-
MINUTE = /\A(?:[01]\d|2[0-3])?:([0-5]\d)\z/
|
|
21
|
-
GEM_ENV = %w[GEM_HOME GEM_PATH].freeze
|
|
22
|
-
SYSTEM_PATH = %w[/usr/bin /bin].freeze
|
|
23
|
-
OS_ID_PREFIX = 'dev.rubyraider.flow'
|
|
24
|
-
KEYS = %w[name every at on retries inputs notify os_id ruby bundle gem_env created_at].freeze
|
|
25
|
-
|
|
26
|
-
attr_reader :name, :every, :at, :on, :retries, :inputs, :notify, :os_id, :ruby, :bundle, :gem_env, :project_root, :created_at
|
|
27
|
-
|
|
28
|
-
def self.build(project:, name:, every:, at: nil, on: nil, retries: 0, inputs: [], notify: false, now: Time.now)
|
|
29
|
-
root = project.root
|
|
30
|
-
new(name: name, every: every, at: at, on: on, retries: retries, inputs: inputs, notify: notify, os_id: os_id_for(root, name),
|
|
31
|
-
ruby: RbConfig.ruby, bundle: bundle_path, gem_env: captured_gem_env,
|
|
32
|
-
project_root: root, created_at: now.getutc.iso8601)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# A schedules.yml entry; raises Error when it is not one this class wrote
|
|
36
|
-
def self.from_h(hash, project_root)
|
|
37
|
-
raise Error, 'A schedule entry must be a mapping' unless hash.is_a?(Hash)
|
|
38
|
-
|
|
39
|
-
fields = KEYS.to_h { |key| [key.to_sym, hash[key]] }
|
|
40
|
-
schedule = new(**fields, project_root: project_root)
|
|
41
|
-
# Strict: an entry copied from another project, or a moved project, is not this project's entry
|
|
42
|
-
raise Error, "Invalid os_id #{schedule.os_id.inspect} for #{schedule.name} in #{project_root}" unless schedule.os_id == os_id_for(project_root, schedule.name)
|
|
43
|
-
|
|
44
|
-
schedule
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def self.os_id_for(project_root, name) = "#{OS_ID_PREFIX}.#{Digest::SHA256.hexdigest(project_root)[0, 10]}.#{name}"
|
|
48
|
-
|
|
49
|
-
def self.bundle_path
|
|
50
|
-
Gem.bin_path('bundler', 'bundle')
|
|
51
|
-
rescue Gem::GemNotFoundException, Gem::MissingSpecError => e
|
|
52
|
-
raise Error, "Bundler not found for this Ruby (#{e.message}): gem install bundler, then try again"
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# The GEM_HOME and GEM_PATH raider started with (before bundle exec changed them), only when set
|
|
56
|
-
def self.captured_gem_env
|
|
57
|
-
env = defined?(Bundler) ? Bundler.original_env : ENV
|
|
58
|
-
GEM_ENV.each_with_object({}) do |key, captured|
|
|
59
|
-
value = env[key]
|
|
60
|
-
captured[key] = value if value.is_a?(String) && !value.empty?
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def initialize(name:, every:, at:, on:, retries:, inputs:, os_id:, ruby:, bundle:, gem_env:, project_root:, created_at:, notify: false)
|
|
65
|
-
@name = checked_name(name)
|
|
66
|
-
@every = checked_every(every)
|
|
67
|
-
@at = checked_at(at)
|
|
68
|
-
@on = checked_on(on)
|
|
69
|
-
@retries = checked_retries(retries)
|
|
70
|
-
@inputs = checked_inputs(inputs)
|
|
71
|
-
@notify = checked_notify(notify)
|
|
72
|
-
@os_id = checked_os_id(os_id)
|
|
73
|
-
@ruby = checked_path('ruby', ruby)
|
|
74
|
-
@bundle = checked_path('bundle', bundle)
|
|
75
|
-
@gem_env = checked_gem_env(gem_env)
|
|
76
|
-
@project_root = checked_path('project_root', project_root)
|
|
77
|
-
@created_at = checked_text('created_at', created_at)
|
|
78
|
-
freeze
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def hour = at[0, 2].to_i
|
|
82
|
-
|
|
83
|
-
def minute = at[3, 2].to_i
|
|
84
|
-
|
|
85
|
-
def days
|
|
86
|
-
case every
|
|
87
|
-
when 'weekday' then DAYS.first(5)
|
|
88
|
-
when 'week' then [on.to_s]
|
|
89
|
-
else DAYS
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def argv
|
|
94
|
-
command = [ruby, bundle, 'exec', 'raider', 'flow', 'run', name, '--project', project_root, '--trigger', 'schedule']
|
|
95
|
-
command += ['--retries', retries.to_s] if retries.positive?
|
|
96
|
-
command << '--notify' if notify
|
|
97
|
-
command += ['--input', *inputs] unless inputs.empty?
|
|
98
|
-
command
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def env
|
|
102
|
-
path = [File.dirname(ruby), File.dirname(bundle), *SYSTEM_PATH].uniq.join(File::PATH_SEPARATOR)
|
|
103
|
-
{ 'PATH' => path, 'LANG' => 'en_US.UTF-8' }.merge(gem_env)
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def working_directory = project_root
|
|
107
|
-
|
|
108
|
-
def log_path = File.join(project_root, '.raider', 'flows', name, 'scheduler.log')
|
|
109
|
-
|
|
110
|
-
def rule_text
|
|
111
|
-
case every
|
|
112
|
-
when 'hour' then "every hour at #{at[2, 3]}"
|
|
113
|
-
when 'week' then "every week on #{on} at #{at}"
|
|
114
|
-
else "every #{every} at #{at}"
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def to_h
|
|
119
|
-
{ 'name' => name, 'every' => every, 'at' => at, 'on' => on, 'retries' => retries, 'inputs' => inputs.dup, 'notify' => notify,
|
|
120
|
-
'os_id' => os_id, 'ruby' => ruby, 'bundle' => bundle, 'gem_env' => gem_env.dup, 'created_at' => created_at }
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
private
|
|
124
|
-
|
|
125
|
-
def checked_name(name)
|
|
126
|
-
return name.dup.freeze if name.is_a?(String) && Flows.valid_name?(name)
|
|
127
|
-
|
|
128
|
-
raise Error, "Invalid flow name #{name.inspect}: use 1 to 60 of a-z, 0-9 and _ (not env)"
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def checked_every(every)
|
|
132
|
-
return every.dup.freeze if EVERY.include?(every)
|
|
133
|
-
|
|
134
|
-
raise Error, "Invalid every #{every.inspect}: use hour, day, weekday or week"
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# HH:MM, or for every hour :MM or HH:MM (the hour ignored, :00 when left out), kept as HH:MM
|
|
138
|
-
def checked_at(at)
|
|
139
|
-
return clock(at) unless every == 'hour'
|
|
140
|
-
return '00:00' if at.nil?
|
|
141
|
-
|
|
142
|
-
match = MINUTE.match(at.to_s)
|
|
143
|
-
raise Error, "Invalid at #{at.inspect}: use :MM or HH:MM for every hour" unless match
|
|
144
|
-
|
|
145
|
-
"00:#{match[1]}"
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def clock(at)
|
|
149
|
-
raise Error, "Every #{every} needs at: use HH:MM (24h)" if at.nil?
|
|
150
|
-
return at.dup.freeze if at.is_a?(String) && CLOCK.match?(at)
|
|
151
|
-
|
|
152
|
-
raise Error, "Invalid at #{at.inspect}: use HH:MM (24h)"
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def checked_on(on)
|
|
156
|
-
unless every == 'week'
|
|
157
|
-
raise Error, 'on is only for every week' unless on.nil?
|
|
158
|
-
|
|
159
|
-
return nil
|
|
160
|
-
end
|
|
161
|
-
raise Error, "Every week needs on: #{DAYS_RULE}" if on.nil?
|
|
162
|
-
return on.dup.freeze if DAYS.include?(on)
|
|
163
|
-
|
|
164
|
-
raise Error, "Invalid on #{on.inspect}: #{DAYS_RULE}"
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def checked_retries(retries)
|
|
168
|
-
return retries if retries.is_a?(Integer) && retries.between?(0, 3)
|
|
169
|
-
|
|
170
|
-
raise Error, "Invalid retries #{retries.inspect}: use 0 to 3"
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
# name=value pairs as `raider flow run --input` takes them; a control character could break an OS entry
|
|
174
|
-
def checked_inputs(inputs)
|
|
175
|
-
raise Error, 'Invalid inputs: use a list of name=value' unless inputs.is_a?(Array)
|
|
176
|
-
|
|
177
|
-
inputs.map do |pair|
|
|
178
|
-
raise Error, "Invalid input #{pair.inspect}: use name=value in valid UTF-8" unless pair.is_a?(String) && pair.valid_encoding?
|
|
179
|
-
|
|
180
|
-
key, value = pair.split('=', 2)
|
|
181
|
-
raise Error, "Invalid input #{key.to_s.inspect}: #{Runner::INPUT_RULE}" unless value && Runner::INPUT.match?(key.to_s)
|
|
182
|
-
raise Error, "Invalid input #{key.inspect}: the value contains a control character" if value.match?(/[[:cntrl:]]/)
|
|
183
|
-
|
|
184
|
-
pair.dup.freeze
|
|
185
|
-
end.freeze
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
# An entry written before notify existed has no key: off
|
|
189
|
-
def checked_notify(notify)
|
|
190
|
-
return notify == true if [true, false, nil].include?(notify)
|
|
191
|
-
|
|
192
|
-
raise Error, "Invalid notify #{notify.inspect}: use true or false"
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def checked_os_id(os_id)
|
|
196
|
-
pattern = /\A#{Regexp.escape(OS_ID_PREFIX)}\.[0-9a-f]{10}\.#{Regexp.escape(name)}\z/
|
|
197
|
-
return os_id.dup.freeze if os_id.is_a?(String) && pattern.match?(os_id)
|
|
198
|
-
|
|
199
|
-
raise Error, "Invalid os_id #{os_id.inspect} for #{name}"
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
def checked_path(label, path)
|
|
203
|
-
return checked_text(label, path) if path.is_a?(String) && File.absolute_path?(path)
|
|
204
|
-
|
|
205
|
-
raise Error, "Invalid #{label} #{path.inspect}: use an absolute path"
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
def checked_gem_env(gem_env)
|
|
209
|
-
raise Error, 'Invalid gem_env: use a mapping of GEM_HOME and GEM_PATH' unless gem_env.is_a?(Hash) && (gem_env.keys - GEM_ENV).empty?
|
|
210
|
-
|
|
211
|
-
gem_env.to_h { |key, value| [key.dup.freeze, checked_text(key, value)] }.freeze
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def checked_text(label, text)
|
|
215
|
-
return text.dup.freeze if text.is_a?(String) && text.valid_encoding? && !text.empty? && !text.match?(/[[:cntrl:]]/)
|
|
216
|
-
|
|
217
|
-
raise Error, "Invalid #{label} #{text.inspect}"
|
|
218
|
-
end
|
|
219
|
-
end
|
|
220
|
-
end
|
|
221
|
-
end
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'fileutils'
|
|
4
|
-
require 'json'
|
|
5
|
-
require 'rbconfig'
|
|
6
|
-
require_relative 'next_run'
|
|
7
|
-
require_relative 'run_log'
|
|
8
|
-
require_relative 'schedule'
|
|
9
|
-
require_relative 'schedule_store'
|
|
10
|
-
require_relative 'os/command_runner'
|
|
11
|
-
require_relative 'os/launchd'
|
|
12
|
-
require_relative 'os/task_scheduler'
|
|
13
|
-
require_relative 'os/linux'
|
|
14
|
-
|
|
15
|
-
module RubyRaider
|
|
16
|
-
module Flows
|
|
17
|
-
# `raider flow schedule | unschedule | schedules`: keeps the OS scheduler entries and
|
|
18
|
-
# .raider/flows/schedules.yml in step. The OS entry is installed first and the yml written after,
|
|
19
|
-
# so the yml never names an entry the OS scheduler refused.
|
|
20
|
-
# :reek:TooManyInstanceVariables { max_instance_variables: 6 } -- the project, its store, the adapter and the injected streams and clock
|
|
21
|
-
class Scheduler
|
|
22
|
-
NOT_SET_UP = 'Flows are not set up in this project: run raider flow init'
|
|
23
|
-
RUBY_NOT_FOUND = 'Ruby not found: re-schedule'
|
|
24
|
-
FLOW_NOT_FOUND = 'flow not found: unschedule'
|
|
25
|
-
LAST = %w[status finished_at trigger failed_step results_dir].freeze
|
|
26
|
-
TIME = '%a %-d %b %H:%M'
|
|
27
|
-
|
|
28
|
-
# The OS scheduler for this machine; only Linux asks the system (is the user's systemd there?)
|
|
29
|
-
def self.adapter(runner: Os::CommandRunner.new, host_os: RbConfig::CONFIG['host_os'])
|
|
30
|
-
case host_os
|
|
31
|
-
when /darwin/ then Os::Launchd.new(runner: runner)
|
|
32
|
-
when /mswin|mingw|cygwin/ then Os::TaskScheduler.new(runner: runner)
|
|
33
|
-
when /linux/ then Os::Linux.pick(runner)
|
|
34
|
-
else raise Error, "Scheduling flows is not supported on #{host_os}"
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# adapter: nil picks the OS scheduler on first use, so a command refused earlier never touches it
|
|
39
|
-
def initialize(project, adapter: nil, store: ScheduleStore.new(project), out: $stdout, err: $stderr, now: -> { Time.now })
|
|
40
|
-
@project = project
|
|
41
|
-
@adapter = adapter
|
|
42
|
-
@store = store
|
|
43
|
-
@out = out
|
|
44
|
-
@err = err
|
|
45
|
-
@now = now
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def schedule(name, every:, at: nil, on: nil, retries: 0, inputs: [], notify: false)
|
|
49
|
-
@project.flow_file(name)
|
|
50
|
-
raise Error, NOT_SET_UP unless @project.set_up?
|
|
51
|
-
|
|
52
|
-
now = @now.call
|
|
53
|
-
schedule = Schedule.build(project: @project, name: name, every: every, at: at, on: on, retries: retries,
|
|
54
|
-
inputs: inputs, notify: notify, now: now)
|
|
55
|
-
flow_dir(name)
|
|
56
|
-
@store.writable!
|
|
57
|
-
old = @store.find(name)
|
|
58
|
-
adapter.install(schedule)
|
|
59
|
-
save(schedule)
|
|
60
|
-
prefix = old ? "Replaced the schedule for #{name}" : "Scheduled #{name}"
|
|
61
|
-
@out.puts("#{prefix}: #{rule(schedule)}. Next run #{NextRun.after(schedule, now).strftime(TIME)}.")
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def unschedule(name)
|
|
65
|
-
raise Error, "Invalid flow name #{name.inspect}: use 1 to 60 of a-z, 0-9 and _ (not env)" unless Flows.valid_name?(name)
|
|
66
|
-
|
|
67
|
-
# The OS entry first: when the scheduler refuses, the yml entry stays to retry with.
|
|
68
|
-
# from_h is strict, so a stored entry's os_id is always this one.
|
|
69
|
-
label = adapter.label
|
|
70
|
-
was_there = adapter.remove(Schedule.os_id_for(@project.root, name))
|
|
71
|
-
removed = delete(name, label)
|
|
72
|
-
raise Error, "No schedule for #{name}" unless removed || was_there
|
|
73
|
-
|
|
74
|
-
@out.puts(was_there ? "Unscheduled #{name}" : "Unscheduled #{name} (it was already gone from #{label})")
|
|
75
|
-
return unless removed.is_a?(ScheduleStore::Removed)
|
|
76
|
-
|
|
77
|
-
@err.puts("The schedules.yml entry for #{name} could not be read: a #{label} entry made from a different project path may remain")
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def schedules(json: false)
|
|
81
|
-
now = @now.call
|
|
82
|
-
listed = @store.all
|
|
83
|
-
rows = listed.map { |schedule| row(schedule, now) } + unlisted_rows(listed.map(&:name))
|
|
84
|
-
return @out.puts(JSON.generate(rows.map { |entry| entry[:json] })) if json
|
|
85
|
-
return @out.puts('No schedules yet: raider flow schedule <name> --every day --at 08:00') if rows.empty?
|
|
86
|
-
|
|
87
|
-
rows.each { |entry| @out.puts(Flows.printable(entry[:text])) }
|
|
88
|
-
note = adapter.respond_to?(:missed_runs_note) ? adapter.missed_runs_note : nil
|
|
89
|
-
@out.puts(note) if note
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
private
|
|
93
|
-
|
|
94
|
-
def adapter = @adapter ||= Scheduler.adapter
|
|
95
|
-
|
|
96
|
-
# Writes the yml; when that fails the OS entry just installed is removed, so neither names the flow
|
|
97
|
-
def save(schedule)
|
|
98
|
-
@store.save(schedule)
|
|
99
|
-
rescue Error, SystemCallError => e
|
|
100
|
-
label = adapter.label
|
|
101
|
-
begin
|
|
102
|
-
adapter.remove(schedule.os_id)
|
|
103
|
-
rescue Error => remove_error
|
|
104
|
-
raise Error, "#{e.message} (and removing the #{label} entry failed: #{remove_error.message})"
|
|
105
|
-
end
|
|
106
|
-
raise Error, "#{e.message}: the #{label} entry was removed too, so #{schedule.name} is not scheduled"
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# The OS entry is gone by now: say so when the yml can't be written
|
|
110
|
-
def delete(name, label)
|
|
111
|
-
@store.delete(name)
|
|
112
|
-
rescue Error, SystemCallError => e
|
|
113
|
-
raise Error, "#{e.message}: the #{label} entry was already removed, but schedules.yml still lists #{name}"
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# .raider/flows/<name>/ for scheduler.log: 0700 folders that are not links, as flows/support/run.rb makes them
|
|
117
|
-
def flow_dir(name)
|
|
118
|
-
[ScheduleStore::DIR, "#{ScheduleStore::DIR}/#{name}"].each { |relative| private_dir(relative) }
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def private_dir(relative)
|
|
122
|
-
dir = @project.path(relative)
|
|
123
|
-
FileUtils.mkdir_p(dir)
|
|
124
|
-
raise Error, "#{relative} must be a folder, not a link" unless File.lstat(dir).directory?
|
|
125
|
-
|
|
126
|
-
File.chmod(0o700, dir)
|
|
127
|
-
rescue SystemCallError => e
|
|
128
|
-
raise Error, "Could not make #{relative}: #{e.message}"
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def row(schedule, now)
|
|
132
|
-
name = schedule.name
|
|
133
|
-
next_run = NextRun.after(schedule, now)
|
|
134
|
-
state = os_state(schedule)
|
|
135
|
-
last = last_run(name)
|
|
136
|
-
json = { 'name' => name, 'every' => schedule.every, 'at' => schedule.at, 'on' => schedule.on,
|
|
137
|
-
'retries' => schedule.retries, 'inputs' => schedule.inputs, 'notify' => schedule.notify, 'next_run_at' => next_run.iso8601,
|
|
138
|
-
'os_state' => state, 'last' => last }
|
|
139
|
-
{ json: json, text: line(name, rule(schedule), "next #{next_run.strftime(TIME)}", last, state) }
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Flows the OS scheduler still runs for this project although schedules.yml has no entry for them
|
|
143
|
-
def unlisted_rows(listed)
|
|
144
|
-
(@project.flow_names - listed).filter_map do |name|
|
|
145
|
-
next unless present?(Schedule.os_id_for(@project.root, name))
|
|
146
|
-
|
|
147
|
-
state = 'missing from schedules.yml'
|
|
148
|
-
last = last_run(name)
|
|
149
|
-
json = { 'name' => name, 'every' => nil, 'at' => nil, 'on' => nil, 'retries' => nil, 'inputs' => nil, 'notify' => nil,
|
|
150
|
-
'next_run_at' => nil, 'os_state' => state, 'last' => last }
|
|
151
|
-
{ json: json, text: line(name, '-', '-', last, state) }
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# A flow whose state can't be read is left out: only a listed schedule gets an unknown row
|
|
156
|
-
def present?(os_id)
|
|
157
|
-
adapter.present?(os_id)
|
|
158
|
-
rescue Error
|
|
159
|
-
false
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# The OS entry runs `<ruby> <bundle> ...`: ruby is executed, bundle is only read by it
|
|
163
|
-
def os_state(schedule)
|
|
164
|
-
ruby = schedule.ruby
|
|
165
|
-
bundle = schedule.bundle
|
|
166
|
-
return RUBY_NOT_FOUND unless File.file?(ruby) && File.executable?(ruby) && File.file?(bundle) && File.readable?(bundle)
|
|
167
|
-
return FLOW_NOT_FOUND unless File.file?(@project.path("flows/#{schedule.name}_flow.rb"))
|
|
168
|
-
|
|
169
|
-
adapter.present?(schedule.os_id) ? 'ok' : "missing from #{adapter.label}"
|
|
170
|
-
rescue Error => e
|
|
171
|
-
"unknown (#{e.message})"
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def rule(schedule) = schedule.notify ? "#{schedule.rule_text} (notify)" : schedule.rule_text
|
|
175
|
-
|
|
176
|
-
def last_run(name)
|
|
177
|
-
last = RunLog.new(@project, name).last_result
|
|
178
|
-
last && LAST.to_h { |key| [key, last[key]] }
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def line(name, rule, next_run, last, state)
|
|
182
|
-
result = last ? "#{last['status']} #{last['finished_at']}" : 'never run'
|
|
183
|
-
"#{name.ljust(30)} #{rule} #{next_run} #{result} #{state}"
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
end
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# Type definitions for FlowCommands
|
|
2
|
-
class FlowCommands < Thor
|
|
3
|
-
def init: () -> void
|
|
4
|
-
def run_flow: (String name) -> void
|
|
5
|
-
def list: () -> void
|
|
6
|
-
def schedule: (String name) -> void
|
|
7
|
-
def unschedule: (String name) -> void
|
|
8
|
-
def schedules: () -> void
|
|
9
|
-
def project: () -> RubyRaider::Flows::Project
|
|
10
|
-
def scheduler: () -> RubyRaider::Flows::Scheduler
|
|
11
|
-
def list_line: (Hash[String, untyped] flow) -> String
|
|
12
|
-
def stop: (RubyRaider::Flows::Error error) -> bot
|
|
13
|
-
end
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
module RubyRaider
|
|
2
|
-
module Flows
|
|
3
|
-
NAME: Regexp
|
|
4
|
-
RESERVED: Array[String]
|
|
5
|
-
|
|
6
|
-
def self.valid_name?: (untyped name) -> bool
|
|
7
|
-
def self.printable: (untyped text) -> String
|
|
8
|
-
|
|
9
|
-
class Error < StandardError
|
|
10
|
-
attr_reader exit_code: Integer
|
|
11
|
-
|
|
12
|
-
def initialize: (String message, ?exit_code: Integer) -> void
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
class Project
|
|
16
|
-
UNSUPPORTED: String
|
|
17
|
-
GITIGNORE_LINES: Array[String]
|
|
18
|
-
TEMPLATES: Hash[String, String]
|
|
19
|
-
PIN: Regexp
|
|
20
|
-
FLOWS_PIN: Array[Integer]
|
|
21
|
-
|
|
22
|
-
attr_reader root: String
|
|
23
|
-
|
|
24
|
-
def initialize: (String path) -> void
|
|
25
|
-
def path: (String relative) -> String
|
|
26
|
-
def flow_file: (String name) -> String
|
|
27
|
-
def flow_names: () -> Array[String]
|
|
28
|
-
def set_up?: () -> bool
|
|
29
|
-
def unsupported_reason: () -> String?
|
|
30
|
-
def init: (?out: IO | StringIO, ?err: IO | StringIO) -> Integer
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
def template_dir: () -> String
|
|
35
|
-
def read: (String relative) -> String?
|
|
36
|
-
def selenium?: (String gemfile) -> bool
|
|
37
|
-
def runtime?: () -> bool
|
|
38
|
-
def old_pin?: (String gemfile) -> bool
|
|
39
|
-
def write_templates: () -> Array[String]
|
|
40
|
-
def add_gitignore_lines: () -> Array[String]
|
|
41
|
-
def pin_gem: () -> bool
|
|
42
|
-
def warn_unknown_pin: (IO | StringIO err) -> void
|
|
43
|
-
def report: (IO | StringIO out, Array[String] created, Array[String] updated, bool pinned) -> void
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|