polyrun 1.3.0 → 1.4.1

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.
@@ -31,7 +31,7 @@ module Polyrun
31
31
  costs, strategy, err = run_shards_resolve_costs(o[:timing_path], o[:strategy], o[:timing_granularity])
32
32
  return [err, nil] if err
33
33
 
34
- run_shards_plan_ready_log(o, strategy, cmd, paths_source, items.size)
34
+ run_shards_plan_ready_log(o, cfg, strategy, cmd, paths_source, items.size)
35
35
 
36
36
  constraints = load_partition_constraints(pc, o[:constraints_path])
37
37
  plan = run_shards_make_plan(items, o[:workers], strategy, o[:seed], costs, constraints, o[:timing_granularity])
@@ -59,12 +59,13 @@ module Polyrun
59
59
  [run_t0, head, cmd, cfg, cfg.partition]
60
60
  end
61
61
 
62
- def run_shards_plan_ready_log(o, strategy, cmd, paths_source, item_count)
62
+ def run_shards_plan_ready_log(o, cfg, strategy, cmd, paths_source, item_count)
63
63
  Polyrun::Debug.log_kv(
64
64
  run_shards: "ready to partition",
65
65
  workers: o[:workers],
66
66
  strategy: strategy,
67
67
  merge_coverage: o[:merge_coverage],
68
+ merge_failures: run_shards_merge_failures_flag(o, cfg),
68
69
  command: cmd,
69
70
  timing_path: o[:timing_path],
70
71
  paths_source: paths_source,
@@ -72,6 +73,37 @@ module Polyrun
72
73
  )
73
74
  end
74
75
 
76
+ def run_shards_merge_failures_flag(o, cfg)
77
+ return true if o[:merge_failures]
78
+ return true if %w[1 true yes].include?(ENV["POLYRUN_MERGE_FAILURES"].to_s.downcase)
79
+
80
+ rep = cfg.reporting
81
+ v = rep["merge_failures"] || rep[:merge_failures]
82
+ v == true || %w[1 true yes].include?(v.to_s.downcase)
83
+ end
84
+
85
+ def run_shards_merge_failures_output_opt(o, cfg)
86
+ x = o[:merge_failures_output]
87
+ return x if x && !x.to_s.strip.empty?
88
+
89
+ x = ENV["POLYRUN_MERGED_FAILURES_OUT"]
90
+ return x if x && !x.to_s.strip.empty?
91
+
92
+ rep = cfg.reporting
93
+ rep["merge_failures_output"] || rep[:merge_failures_output]
94
+ end
95
+
96
+ def run_shards_merge_failures_format_opt(o, cfg)
97
+ x = o[:merge_failures_format]
98
+ return x if x && !x.to_s.strip.empty?
99
+
100
+ x = ENV["POLYRUN_MERGED_FAILURES_FORMAT"]
101
+ return x if x && !x.to_s.strip.empty?
102
+
103
+ rep = cfg.reporting
104
+ rep["merge_failures_format"] || rep[:merge_failures_format]
105
+ end
106
+
75
107
  def run_shards_plan_context_hash(o, cmd, cfg, plan, run_t0, parallel, config_path)
76
108
  {
77
109
  workers: o[:workers],
@@ -83,6 +115,9 @@ module Polyrun
83
115
  merge_coverage: o[:merge_coverage],
84
116
  merge_output: o[:merge_output],
85
117
  merge_format: o[:merge_format],
118
+ merge_failures: run_shards_merge_failures_flag(o, cfg),
119
+ merge_failures_output: run_shards_merge_failures_output_opt(o, cfg),
120
+ merge_failures_format: run_shards_merge_failures_format_opt(o, cfg),
86
121
  config_path: config_path
87
122
  }
88
123
  end
@@ -24,7 +24,10 @@ module Polyrun
24
24
  timing_granularity: nil,
25
25
  merge_coverage: false,
26
26
  merge_output: nil,
27
- merge_format: nil
27
+ merge_format: nil,
28
+ merge_failures: false,
29
+ merge_failures_output: nil,
30
+ merge_failures_format: nil
28
31
  }
29
32
  end
30
33
 
@@ -34,8 +37,9 @@ module Polyrun
34
37
  end.parse!(head)
35
38
  end
36
39
 
40
+ # rubocop:disable Metrics/AbcSize -- one argv block for run-shards
37
41
  def run_shards_plan_options_register!(opts, st)
38
- opts.banner = "usage: polyrun run-shards [--workers N] [--strategy NAME] [--paths-file P] [--timing P] [--timing-granularity VAL] [--constraints P] [--seed S] [--merge-coverage] [--merge-output P] [--merge-format LIST] [--] <command> [args...]"
42
+ opts.banner = "usage: polyrun run-shards [--workers N] [--strategy NAME] [--paths-file P] [--timing P] [--timing-granularity VAL] [--constraints P] [--seed S] [--merge-coverage] [--merge-output P] [--merge-format LIST] [--merge-failures] [--merge-failures-output P] [--merge-failures-format jsonl|json] [--] <command> [args...]"
39
43
  opts.on("--workers N", Integer) { |v| st[:workers] = v }
40
44
  opts.on("--strategy NAME", String) { |v| st[:strategy] = v }
41
45
  opts.on("--seed VAL") { |v| st[:seed] = v }
@@ -46,7 +50,11 @@ module Polyrun
46
50
  opts.on("--merge-coverage", "After success, merge coverage/polyrun-fragment-*.json (Polyrun coverage must be enabled)") { st[:merge_coverage] = true }
47
51
  opts.on("--merge-output PATH", String) { |v| st[:merge_output] = v }
48
52
  opts.on("--merge-format LIST", String) { |v| st[:merge_format] = v }
53
+ opts.on("--merge-failures", "After all workers exit, merge tmp/polyrun_failures/polyrun-failure-fragment-*.jsonl (use Polyrun::RSpec.install_failure_fragments!)") { st[:merge_failures] = true }
54
+ opts.on("--merge-failures-output PATH", String) { |v| st[:merge_failures_output] = v }
55
+ opts.on("--merge-failures-format VAL", "jsonl (default) or json") { |v| st[:merge_failures_format] = v }
49
56
  end
57
+ # rubocop:enable Metrics/AbcSize
50
58
  end
51
59
  end
52
60
  end
@@ -2,12 +2,14 @@ require "shellwords"
2
2
  require "rbconfig"
3
3
 
4
4
  require_relative "run_shards_planning"
5
+ require_relative "run_shards_parallel_children"
5
6
 
6
7
  module Polyrun
7
8
  class CLI
8
9
  # Partition + spawn workers for `polyrun run-shards` (keeps {RunShardsCommand} file small).
9
10
  module RunShardsRun
10
11
  include RunShardsPlanning
12
+ include RunShardsParallelChildren
11
13
 
12
14
  private
13
15
 
@@ -18,59 +20,84 @@ module Polyrun
18
20
  run_shards_workers_and_merge(ctx)
19
21
  end
20
22
 
23
+ # rubocop:disable Metrics/AbcSize -- orchestration: hooks, merge, worker failures
21
24
  def run_shards_workers_and_merge(ctx)
22
- pids = run_shards_spawn_workers(ctx)
23
- return 1 if pids.empty?
24
-
25
- run_shards_warn_interleaved(ctx[:parallel], pids.size)
25
+ hook_cfg = Polyrun::Hooks.from_config(ctx[:cfg])
26
+ suite_started = false
27
+ exit_code = 1
28
+ merged_failures_path = nil
29
+ merge_failures_errored = false
30
+
31
+ begin
32
+ env_suite = ENV.to_h.merge(
33
+ "POLYRUN_HOOK_ORCHESTRATOR" => "1",
34
+ "POLYRUN_SHARD_TOTAL" => ctx[:workers].to_s
35
+ )
36
+ code = hook_cfg.run_phase_if_enabled(:before_suite, env_suite)
37
+ return code if code != 0
38
+
39
+ suite_started = true
40
+
41
+ pids, spawn_err = run_shards_spawn_workers(ctx, hook_cfg)
42
+ if spawn_err
43
+ exit_code = spawn_err
44
+ return spawn_err
45
+ end
46
+ if pids.empty?
47
+ exit_code = 1
48
+ return 1
49
+ end
26
50
 
27
- shard_results = run_shards_wait_all_children(pids)
28
- failed = shard_results.reject { |r| r[:success] }.map { |r| r[:shard] }
51
+ run_shards_warn_interleaved(ctx[:parallel], pids.size)
29
52
 
30
- Polyrun::Debug.log(format(
31
- "run-shards: workers wall time since start: %.3fs",
32
- Process.clock_gettime(Process::CLOCK_MONOTONIC) - ctx[:run_t0]
33
- ))
53
+ shard_results, wait_hook_err = run_shards_wait_all_children(pids, hook_cfg, ctx)
54
+ failed = shard_results.reject { |r| r[:success] }.map { |r| r[:shard] }
34
55
 
35
- if ctx[:parallel]
36
- Polyrun::Log.warn "polyrun run-shards: finished #{pids.size} worker(s)" + (failed.any? ? " (some failed)" : " (exit 0)")
37
- end
56
+ Polyrun::Debug.log(format(
57
+ "run-shards: workers wall time since start: %.3fs",
58
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) - ctx[:run_t0]
59
+ ))
38
60
 
39
- if failed.any?
40
- run_shards_log_failed_reruns(failed, shard_results, ctx[:plan], ctx[:parallel], ctx[:workers], ctx[:cmd])
41
- return 1
42
- end
43
-
44
- run_shards_merge_or_hint_coverage(ctx)
45
- end
61
+ if ctx[:parallel]
62
+ Polyrun::Log.warn "polyrun run-shards: finished #{pids.size} worker(s)" + (failed.any? ? " (some failed)" : " (exit 0)")
63
+ end
46
64
 
47
- def run_shards_spawn_workers(ctx)
48
- workers = ctx[:workers]
49
- cmd = ctx[:cmd]
50
- cfg = ctx[:cfg]
51
- plan = ctx[:plan]
52
- parallel = ctx[:parallel]
53
- mx = ctx[:matrix_shard_index]
54
- mt = ctx[:matrix_shard_total]
55
-
56
- pids = []
57
- workers.times do |shard|
58
- paths = plan.shard(shard)
59
- if paths.empty?
60
- Polyrun::Log.warn "polyrun run-shards: shard #{shard} skipped (no paths)" if @verbose || parallel
61
- next
65
+ if ctx[:merge_failures]
66
+ begin
67
+ merged_failures_path = merge_failures_after_shards(ctx)
68
+ rescue Polyrun::Error => e
69
+ Polyrun::Log.warn e.message.to_s
70
+ merge_failures_errored = true
71
+ end
62
72
  end
63
73
 
64
- child_env = shard_child_env(cfg: cfg, workers: workers, shard: shard, matrix_index: mx, matrix_total: mt)
74
+ if failed.any?
75
+ run_shards_log_failed_reruns(
76
+ failed, shard_results, ctx[:plan], ctx[:parallel], ctx[:workers], ctx[:cmd],
77
+ merge_failures: ctx[:merge_failures]
78
+ )
79
+ exit_code = 1
80
+ exit_code = 1 if wait_hook_err != 0
81
+ return exit_code
82
+ end
65
83
 
66
- Polyrun::Log.warn "polyrun run-shards: shard #{shard} → #{paths.size} file(s)" if @verbose
67
- pid = Process.spawn(child_env, *cmd, *paths)
68
- pids << {pid: pid, shard: shard}
69
- Polyrun::Debug.log("[parent pid=#{$$}] run-shards: Process.spawn shard=#{shard} child_pid=#{pid} spec_files=#{paths.size}")
70
- Polyrun::Log.warn "polyrun run-shards: started shard #{shard} pid=#{pid} (#{paths.size} file(s))" if parallel
84
+ exit_code = run_shards_merge_or_hint_coverage(ctx)
85
+ exit_code = 1 if wait_hook_err != 0 && exit_code == 0
86
+ exit_code = 1 if merge_failures_errored && exit_code == 0
87
+ exit_code
88
+ ensure
89
+ if suite_started
90
+ env_after = ENV.to_h.merge(
91
+ "POLYRUN_HOOK_ORCHESTRATOR" => "1",
92
+ "POLYRUN_SHARD_TOTAL" => ctx[:workers].to_s,
93
+ "POLYRUN_SUITE_EXIT_STATUS" => exit_code.to_s,
94
+ "POLYRUN_MERGED_FAILURES_PATH" => merged_failures_path.to_s
95
+ )
96
+ hook_cfg.run_phase_if_enabled(:after_suite, env_after)
97
+ end
71
98
  end
72
- pids
73
99
  end
100
+ # rubocop:enable Metrics/AbcSize
74
101
 
75
102
  def run_shards_warn_interleaved(parallel, pid_count)
76
103
  return unless parallel && pid_count > 1
@@ -79,27 +106,6 @@ module Polyrun
79
106
  Polyrun::Log.warn "polyrun run-shards: each worker prints its own summary line; the last \"N examples\" line is not a total across shards."
80
107
  end
81
108
 
82
- def run_shards_wait_all_children(pids)
83
- shard_results = []
84
- Polyrun::Debug.time("Process.wait (#{pids.size} worker process(es))") do
85
- pids.each do |h|
86
- Process.wait(h[:pid])
87
- exitstatus = $?.exitstatus
88
- ok = $?.success?
89
- Polyrun::Debug.log("[parent pid=#{$$}] run-shards: Process.wait child_pid=#{h[:pid]} shard=#{h[:shard]} exit=#{exitstatus} success=#{ok}")
90
- shard_results << {shard: h[:shard], exitstatus: exitstatus, success: ok}
91
- end
92
- rescue Interrupt
93
- # Do not trap SIGINT: Process.wait raises Interrupt; a trap races and prints Interrupt + SystemExit traces.
94
- run_shards_shutdown_on_signal!(pids, 130)
95
- rescue SignalException => e
96
- raise unless e.signm == "SIGTERM"
97
-
98
- run_shards_shutdown_on_signal!(pids, 143)
99
- end
100
- shard_results
101
- end
102
-
103
109
  # Best-effort worker teardown then exit. Does not return.
104
110
  def run_shards_shutdown_on_signal!(pids, code)
105
111
  run_shards_terminate_children!(pids)
@@ -149,7 +155,7 @@ module Polyrun
149
155
  0
150
156
  end
151
157
 
152
- def run_shards_log_failed_reruns(failed, shard_results, plan, parallel, workers, cmd)
158
+ def run_shards_log_failed_reruns(failed, shard_results, plan, parallel, workers, cmd, merge_failures: false)
153
159
  exit_by_shard = shard_results.each_with_object({}) { |r, h| h[r[:shard]] = r[:exitstatus] }
154
160
  failed_detail = failed.sort.map { |s| "#{s} (exit #{exit_by_shard[s]})" }.join(", ")
155
161
  Polyrun::Log.warn "polyrun run-shards: failed shard(s): #{failed_detail}"
@@ -164,6 +170,9 @@ module Polyrun
164
170
  rerun << Shellwords.join(cmd + paths)
165
171
  Polyrun::Log.warn "polyrun run-shards: shard #{s} re-run (same spec list, no interleave): #{rerun}"
166
172
  end
173
+ unless merge_failures
174
+ Polyrun::Log.warn "polyrun run-shards: one merged failure report — use run-shards --merge-failures with Polyrun::RSpec.install_failure_fragments!; POLYRUN_MERGED_FAILURES_PATH is set on after_suite when merge runs."
175
+ end
167
176
  end
168
177
  end
169
178
  end
data/lib/polyrun/cli.rb CHANGED
@@ -16,6 +16,7 @@ require_relative "cli/ci_shard_run_parse"
16
16
  require_relative "cli/ci_shard_run_command"
17
17
  require_relative "cli/config_command"
18
18
  require_relative "cli/default_run"
19
+ require_relative "cli/hooks_command"
19
20
  require_relative "cli/help"
20
21
 
21
22
  module Polyrun
@@ -27,9 +28,9 @@ module Polyrun
27
28
 
28
29
  # Keep in sync with +dispatch_cli_command_subcommands+ (+when+ branches). Used for implicit path routing.
29
30
  DISPATCH_SUBCOMMAND_NAMES = %w[
30
- plan prepare merge-coverage report-coverage report-junit report-timing
31
+ plan prepare merge-coverage merge-failures report-coverage report-junit report-timing
31
32
  env config merge-timing db:setup-template db:setup-shard db:clone-shards
32
- run-shards parallel-rspec start build-paths init queue quick
33
+ run-shards parallel-rspec start build-paths init queue quick hook
33
34
  ].freeze
34
35
 
35
36
  # First argv token that is a normal subcommand (not a path); if argv[0] is not here but looks like paths, run implicit parallel.
@@ -53,6 +54,7 @@ module Polyrun
53
54
  include CiShardRunCommand
54
55
  include ConfigCommand
55
56
  include DefaultRun
57
+ include HooksCommand
56
58
  include Help
57
59
 
58
60
  def self.run(argv = ARGV)
@@ -143,6 +145,8 @@ module Polyrun
143
145
  cmd_prepare(argv, config_path)
144
146
  when "merge-coverage"
145
147
  cmd_merge_coverage(argv, config_path)
148
+ when "merge-failures"
149
+ cmd_merge_failures(argv, config_path)
146
150
  when "report-coverage"
147
151
  cmd_report_coverage(argv)
148
152
  when "report-junit"
@@ -177,6 +181,8 @@ module Polyrun
177
181
  cmd_queue(argv)
178
182
  when "quick"
179
183
  cmd_quick(argv)
184
+ when "hook"
185
+ cmd_hook(argv, config_path)
180
186
  else
181
187
  Polyrun::Log.warn "unknown command: #{command}"
182
188
  2
@@ -61,6 +61,16 @@ module Polyrun
61
61
  def version
62
62
  raw["version"] || raw[:version]
63
63
  end
64
+
65
+ # Optional +hooks:+ block for +run-shards+ / +parallel-rspec+ / +ci-shard-*+ (see {Hooks}).
66
+ def hooks
67
+ raw["hooks"] || raw[:hooks] || {}
68
+ end
69
+
70
+ # Optional +reporting:+ block (merge-failures output paths, etc.).
71
+ def reporting
72
+ raw["reporting"] || raw[:reporting] || {}
73
+ end
64
74
  end
65
75
  end
66
76
 
@@ -0,0 +1,128 @@
1
+ require "monitor"
2
+
3
+ require_relative "../log"
4
+
5
+ module Polyrun
6
+ class Hooks
7
+ # Ruby DSL for +hooks.ruby+ / +hooks.ruby_file+ in +polyrun.yml+ (see README).
8
+ #
9
+ # Example file (+config/polyrun_hooks.rb+):
10
+ #
11
+ # before(:suite) { |env| puts env["POLYRUN_HOOK_PHASE"] }
12
+ # after(:each) { |env| }
13
+ #
14
+ # Blocks receive a hash with string keys (same env as shell hooks).
15
+ module Dsl
16
+ class Registry
17
+ def initialize
18
+ @phases = Hash.new { |h, k| h[k] = [] }
19
+ end
20
+
21
+ def add(phase, proc)
22
+ @phases[phase.to_sym] << proc
23
+ end
24
+
25
+ # @param env [Hash] string-keyed env
26
+ def run(phase, env)
27
+ @phases[phase.to_sym].each { |pr| pr.call(env) }
28
+ end
29
+
30
+ def any?(phase)
31
+ @phases[phase.to_sym].any?
32
+ end
33
+
34
+ def empty?
35
+ @phases.values.all?(&:empty?)
36
+ end
37
+
38
+ def worker_hooks?
39
+ any?(:before_worker) || any?(:after_worker)
40
+ end
41
+ end
42
+
43
+ # Evaluates a hook file with +before+ / +after+ (+before(:suite)+, etc.).
44
+ class FileContext
45
+ def initialize(path)
46
+ @path = path
47
+ @registry = Registry.new
48
+ end
49
+
50
+ attr_reader :registry
51
+
52
+ def load_file
53
+ instance_eval(File.read(@path), @path)
54
+ @registry
55
+ end
56
+
57
+ def before(sym, &block)
58
+ @registry.add(map_before(sym), block)
59
+ end
60
+
61
+ def after(sym, &block)
62
+ @registry.add(map_after(sym), block)
63
+ end
64
+
65
+ private
66
+
67
+ def map_before(sym)
68
+ case sym.to_sym
69
+ when :suite then :before_suite
70
+ when :all then :before_shard
71
+ when :each then :before_worker
72
+ else
73
+ raise ArgumentError, "hooks DSL: before(#{sym.inspect}) — use :suite, :all, or :each"
74
+ end
75
+ end
76
+
77
+ def map_after(sym)
78
+ case sym.to_sym
79
+ when :suite then :after_suite
80
+ when :all then :after_shard
81
+ when :each then :after_worker
82
+ else
83
+ raise ArgumentError, "hooks DSL: after(#{sym.inspect}) — use :suite, :all, or :each"
84
+ end
85
+ end
86
+ end
87
+
88
+ class << self
89
+ # @return [Registry, nil]
90
+ def load_registry(path)
91
+ return nil if path.nil? || path.to_s.strip.empty?
92
+
93
+ full = File.expand_path(path.to_s, Dir.pwd)
94
+ return nil unless File.file?(full)
95
+
96
+ mtime = File.mtime(full)
97
+ cache_mu.synchronize do
98
+ hit = registry_cache[full]
99
+ return hit[:registry] if hit && hit[:mtime] == mtime
100
+
101
+ reg = FileContext.new(full).load_file
102
+ registry_cache[full] = {mtime: mtime, registry: reg}
103
+ reg
104
+ end
105
+ rescue => e
106
+ Polyrun::Log.warn "polyrun hooks: failed to load #{full}: #{e.class}: #{e.message}"
107
+ nil
108
+ end
109
+
110
+ def clear_cache!
111
+ cache_mu.synchronize { registry_cache.clear }
112
+ end
113
+
114
+ private
115
+
116
+ # rubocop:disable ThreadSafety/ClassInstanceVariable -- single-threaded hook load; Monitor protects cache
117
+ def cache_mu
118
+ @cache_mu ||= Monitor.new
119
+ end
120
+
121
+ def registry_cache
122
+ @registry_cache ||= {}
123
+ end
124
+ # rubocop:enable ThreadSafety/ClassInstanceVariable
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,27 @@
1
+ require_relative "../log"
2
+
3
+ module Polyrun
4
+ class Hooks
5
+ # Invoked by worker shell wrapper (+ruby -e+). Requires +POLYRUN_HOOKS_RUBY_FILE+.
6
+ module WorkerRunner
7
+ module_function
8
+
9
+ # @return [Integer] exit code (0 on success)
10
+ def run!(phase)
11
+ phase = phase.to_sym
12
+ path = ENV["POLYRUN_HOOKS_RUBY_FILE"]
13
+ return 0 if path.nil? || path.empty?
14
+
15
+ registry = Dsl.load_registry(path)
16
+ return 0 if registry.nil? || !registry.any?(phase)
17
+
18
+ env = ENV.to_h
19
+ registry.run(phase, env)
20
+ 0
21
+ rescue => e
22
+ Polyrun::Log.warn "polyrun hooks worker #{phase}: #{e.class}: #{e.message}"
23
+ 1
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,50 @@
1
+ require "shellwords"
2
+ require "rbconfig"
3
+
4
+ module Polyrun
5
+ class Hooks
6
+ # Builds +sh -c+ script for worker processes (shell + Ruby +before_worker+ / +after_worker+).
7
+ module WorkerShell
8
+ # @param cmd [Array<String>] argv before paths
9
+ # @param paths [Array<String>]
10
+ # @return [String] shell script body for +sh -c+ (worker process)
11
+ # rubocop:disable Metrics/AbcSize -- shell + ruby worker hook branches
12
+ def build_worker_shell_script(cmd, paths)
13
+ main = Shellwords.join(cmd + paths)
14
+ rb = RbConfig.ruby
15
+ bw_shell = commands_for(:before_worker)
16
+ aw_shell = commands_for(:after_worker)
17
+ bw_ruby = ruby_registry&.any?(:before_worker)
18
+ aw_ruby = ruby_registry&.any?(:after_worker)
19
+
20
+ lines = []
21
+ lines << "export POLYRUN_HOOK_PHASE=before_worker"
22
+ if bw_ruby || bw_shell.any?
23
+ lines << "set -e"
24
+ lines << worker_ruby_line(rb, :before_worker) if bw_ruby
25
+ bw_shell.each { |c| lines << c }
26
+ end
27
+ lines << "set +e"
28
+ lines << main
29
+ lines << "ec=$?"
30
+ lines << "export POLYRUN_HOOK_PHASE=after_worker"
31
+ if aw_ruby || aw_shell.any?
32
+ lines << "set +e"
33
+ aw_shell.each { |c| lines << "( #{c} ) || true" }
34
+ lines << worker_ruby_line(rb, :after_worker, wrap_allow_fail: true) if aw_ruby
35
+ end
36
+ lines << "exit $ec"
37
+ lines.join("\n")
38
+ end
39
+ # rubocop:enable Metrics/AbcSize
40
+
41
+ private
42
+
43
+ def worker_ruby_line(rb_exe, phase, wrap_allow_fail: false)
44
+ code = %(require "polyrun"; Polyrun::Hooks::WorkerRunner.run!(:#{phase}))
45
+ line = "#{rb_exe} -e #{Shellwords.escape(code)}"
46
+ wrap_allow_fail ? "( #{line} ) || true" : line
47
+ end
48
+ end
49
+ end
50
+ end