Class: Polyrun::Hooks

Inherits:
Object
  • Object
show all
Includes:
WorkerShell
Defined in:
lib/polyrun/hooks.rb,
lib/polyrun/hooks/dsl.rb,
lib/polyrun/hooks/worker_shell.rb,
lib/polyrun/hooks/worker_runner.rb

Overview

Shell and Ruby DSL hooks around parallel orchestration, named like RSpec lifecycle callbacks: before_suite / after_suite (+before(:suite)+ / after(:suite)), before_shard / after_shard (parent process, per partition index), before_worker / after_worker (inside each worker process, around the test command).

Configure under hooks: in polyrun.yml: shell strings, and/or ruby: path to a Ruby DSL file. Run manually: polyrun hook run <phase> (see CLI).

Orchestration respects POLYRUN_HOOKS_DISABLE=1 (run_phase_if_enabled); polyrun hook run always runs #run_phase.

Defined Under Namespace

Modules: Dsl, WorkerRunner, WorkerShell

Constant Summary collapse

PHASES =
%i[
  before_suite after_suite
  before_shard after_shard
  before_worker after_worker
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from WorkerShell

#build_worker_shell_script

Constructor Details

#initialize(raw) ⇒ Hooks

Returns a new instance of Hooks.

Parameters:

  • raw (Hash)

    hooks block from YAML



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/polyrun/hooks.rb', line 52

def initialize(raw)
  @ruby_file = extract_ruby_file(raw)
  h = {}
  raw.each do |k, v|
    next if %w[ruby ruby_file].include?(k.to_s)

    ck = canonical_key(k)
    next if ck.nil?

    h[ck] = v
  end
  @raw = h.freeze
  @ruby_registry_loaded = false
  @ruby_registry = nil
end

Instance Attribute Details

#ruby_fileObject (readonly)

Returns the value of attribute ruby_file.



27
28
29
# File 'lib/polyrun/hooks.rb', line 27

def ruby_file
  @ruby_file
end

Class Method Details

.disabled?Boolean

Returns:

  • (Boolean)


29
30
31
32
# File 'lib/polyrun/hooks.rb', line 29

def self.disabled?
  v = ENV["POLYRUN_HOOKS_DISABLE"].to_s.downcase
  %w[1 true yes].include?(v)
end

.from_config(cfg) ⇒ Object



41
42
43
44
# File 'lib/polyrun/hooks.rb', line 41

def self.from_config(cfg)
  raw = cfg.respond_to?(:hooks) ? cfg.hooks : {}
  new(raw.is_a?(Hash) ? raw : {})
end

.parse_phase(str) ⇒ Object

Maps CLI or YAML-style names (before_suite, “before(:suite)”) to a phase symbol or nil.



47
48
49
# File 'lib/polyrun/hooks.rb', line 47

def self.parse_phase(str)
  new({}).send(:canonical_key, str)
end

.suite_per_matrix_job?Boolean

When POLYRUN_SHARD_TOTAL is greater than 1 (ci-shard-run matrix), suite hooks are skipped by default; set POLYRUN_HOOKS_SUITE_PER_MATRIX_JOB=1 to run before_suite / after_suite on every matrix job (legacy).

Returns:

  • (Boolean)


36
37
38
39
# File 'lib/polyrun/hooks.rb', line 36

def self.suite_per_matrix_job?
  v = ENV["POLYRUN_HOOKS_SUITE_PER_MATRIX_JOB"].to_s.downcase
  %w[1 true yes].include?(v)
end

Instance Method Details

#commands_for(phase) ⇒ Array<String>

Parameters:

  • phase (Symbol)

Returns:

  • (Array<String>)


96
97
98
99
100
101
102
103
104
105
# File 'lib/polyrun/hooks.rb', line 96

def commands_for(phase)
  v = @raw[phase.to_sym]
  case v
  when nil then []
  when Array then v.map(&:to_s).map(&:strip).reject(&:empty?)
  else
    s = v.to_s.strip
    s.empty? ? [] : [s]
  end
end

#empty?Boolean

Returns:

  • (Boolean)


68
69
70
71
72
73
74
75
76
77
# File 'lib/polyrun/hooks.rb', line 68

def empty?
  no_shell = PHASES.all? { |p| commands_for(p).empty? }
  return false unless no_shell

  return true if @ruby_file.nil? || @ruby_file.to_s.strip.empty?
  return true unless File.file?(File.expand_path(@ruby_file, Dir.pwd))

  reg = ruby_registry
  reg.nil? || reg.empty?
end

#merge_worker_ruby_env(env) ⇒ Object

Merges POLYRUN_HOOKS_RUBY_FILE when a DSL file is configured (for worker ruby -e).



86
87
88
89
90
91
92
# File 'lib/polyrun/hooks.rb', line 86

def merge_worker_ruby_env(env)
  return env unless @ruby_file
  abs = File.expand_path(@ruby_file, Dir.pwd)
  return env unless File.file?(abs)

  env.merge("POLYRUN_HOOKS_RUBY_FILE" => abs)
end

#run_phase(phase, env) ⇒ Integer

Runs Ruby DSL blocks (if any), then shell commands for phase.

Returns:

  • (Integer)

    exit code (0 if no commands)



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/polyrun/hooks.rb', line 109

def run_phase(phase, env)
  return 0 unless PHASES.include?(phase.to_sym)

  merged = stringify_env_for_hook(env).merge(
    "POLYRUN_HOOK_PHASE" => phase.to_s,
    "POLYRUN_HOOK" => "1"
  )

  reg = ruby_registry
  if reg&.any?(phase)
    begin
      reg.run(phase, merged)
    rescue Interrupt
      Polyrun::Log.warn "polyrun hooks: #{phase} ruby hook interrupted"
      return 130
    rescue => e
      Polyrun::Log.warn "polyrun hooks: #{phase} ruby hook failed: #{e.class}: #{e.message}"
      return 1
    end
  end

  commands_for(phase).each do |cmd|
    ok = begin
      system(merged, "sh", "-c", cmd)
    rescue Interrupt
      Polyrun::Log.warn "polyrun hooks: #{phase} shell hook interrupted"
      return 130
    end
    return $?.exitstatus unless ok
  end
  0
end

#run_phase_if_enabled(phase, env) ⇒ Object

Like #run_phase, but no-ops when disabled? (+POLYRUN_HOOKS_DISABLE=1+). Used by run-shards / ci-shard orchestration.



143
144
145
146
147
# File 'lib/polyrun/hooks.rb', line 143

def run_phase_if_enabled(phase, env)
  return 0 if self.class.disabled?

  run_phase(phase, env)
end

#worker_hooks?Boolean

Returns:

  • (Boolean)


79
80
81
82
83
# File 'lib/polyrun/hooks.rb', line 79

def worker_hooks?
  return true if commands_for(:before_worker).any? || commands_for(:after_worker).any?

  !!ruby_registry&.worker_hooks?
end