Class: Clacky::DeployTools::SetDeployVariables

Inherits:
Object
  • Object
show all
Defined in:
lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb

Overview

Set environment variables on a Railway service via ‘railway variables –set`. Uses RAILWAY_TOKEN passed through environment — no clackycli wrapper needed.

Supports both normal key=value pairs and Railway inter-service references like ${postgres{postgres.DATABASE_PUBLIC_URL} (pass raw_value: true to skip escaping).

Constant Summary collapse

SENSITIVE_PATTERNS =
[
  /password/i, /secret/i, /api_key/i,
  /token/i,    /credential/i, /private_key/i
].freeze
BATCH_SIZE =

Maximum number of variables to set in a single batch call

20
MAX_RETRIES =

Retry config for transient failures

3
RETRY_DELAY =

seconds

2

Class Method Summary collapse

Class Method Details

.execute(service_name:, variables:, platform_token:, raw_value: false) ⇒ Hash

Set one or more environment variables on a Railway service. Batches all variables into a single ‘railway variables` call to minimize network connections and avoid SSL reset issues.

}

Parameters:

  • service_name (String)

    Railway service name

  • variables (Hash)

    KEY => VALUE pairs

  • platform_token (String)

    RAILWAY_TOKEN for this deploy task

  • raw_value (Boolean) (defaults to: false)

    when true, values are passed unquoted (for Railway ${…} references)

Returns:

  • (Hash)

    { success: Boolean, set_variables: Array<String>, errors: Array<Hash>



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb', line 40

def self.execute(service_name:, variables:, platform_token:, raw_value: false)
  if service_name.nil? || service_name.strip.empty?
    return { success: false, error: "service_name is required" }
  end

  env = ENV.to_h.merge("RAILWAY_TOKEN" => platform_token)

  # Log all variables being set
  variables.each do |key, value|
    log_value = sensitive?(key) ? "******" : value
    puts "  Setting #{key}=#{log_value}"
  end

  # Split into batches to avoid command line length limits
  set_vars   = []
  error_list = []
  var_pairs  = variables.map { |k, v| [k.to_s, v.to_s] }

  var_pairs.each_slice(BATCH_SIZE) do |batch|
    result = set_batch(env, service_name, batch, raw_value: raw_value)
    if result[:success]
      set_vars.concat(batch.map(&:first))
    else
      # Retry logic: attempt individual vars if batch fails
      batch.each do |key, value|
        individual = set_one_with_retry(env, service_name, key, value, raw_value: raw_value)
        if individual[:success]
          set_vars << key
        else
          error_list << { key: key, error: individual[:error] }
        end
      end
    end
  end

  {
    success:       error_list.empty?,
    set_variables: set_vars,
    errors:        error_list
  }
end

.set_batch(env, service_name, pairs, raw_value: false) ⇒ Hash

Set a batch of variables in a single railway command call.

Parameters:

  • env (Hash)

    environment variables

  • service_name (String)

    Railway service name

  • pairs (Array<Array>)
    [key, value], …

Returns:

  • (Hash)

    { success: true } or { success: false, error: String }



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb', line 88

def self.set_batch(env, service_name, pairs, raw_value: false)
  # Each --set argument is passed as a separate array element
  set_flags = pairs.flat_map { |key, value| ["--set", "#{key}=#{value}"] }
  cmd = ["railway", "variables", "--service", service_name, "--skip-deploys"] + set_flags

  # Debug: print the full command being executed
  puts "  [DEBUG] Executing Railway CLI command:"
  puts "  [DEBUG] Array form: #{cmd.inspect}"
  puts "  [DEBUG] Shell form: #{cmd.join(' ')}"
  puts "  [DEBUG] with RAILWAY_TOKEN=#{env['RAILWAY_TOKEN']}" if env['RAILWAY_TOKEN']
  $stdout.flush

  # Use system() instead of Open3.capture3 to avoid stdin/stdout blocking issues
  # system() inherits the current process's stdin/stdout/stderr directly
  require 'timeout'
  
  begin
    success = Timeout.timeout(30) do
      # Close stdin, suppress stdout, but keep stderr visible
      system(env, *cmd, in: :close, out: File::NULL)
    end
  rescue Timeout::Error
    return { success: false, error: "Railway CLI command timed out after 30 seconds" }
  end

  if success
    { success: true }
  else
    { success: false, error: "railway variables command failed (exit code: #{$?.exitstatus})" }
  end
end

.set_one(env, service_name, key, value, raw_value: false) ⇒ Hash

Set a single variable. Builds the ‘railway variables –set` command.

Returns:

  • (Hash)

    { success: true } or { success: false, error: String }



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb', line 144

def self.set_one(env, service_name, key, value, raw_value: false)
  assignment = "#{key}=#{value}"

  cmd = [
    "railway", "variables",
    "--service", service_name,
    "--skip-deploys",
    "--set", assignment
  ]

  # Debug: print the full command being executed
  puts "  [DEBUG] Executing single var Railway CLI command:"
  puts "  [DEBUG] Array form: #{cmd.inspect}"
  puts "  [DEBUG] Shell form: #{cmd.join(' ')}"
  puts "  [DEBUG] with RAILWAY_TOKEN=#{env['RAILWAY_TOKEN']}" if env['RAILWAY_TOKEN']
  $stdout.flush

  # Use system() instead of Open3.capture3 to avoid stdin/stdout blocking issues
  require 'timeout'
  
  begin
    success = Timeout.timeout(30) do
      # Close stdin, suppress stdout, but keep stderr visible
      system(env, *cmd, in: :close, out: File::NULL)
    end
  rescue Timeout::Error
    return { success: false, error: "Railway CLI command timed out after 30 seconds" }
  end

  if success
    { success: true }
  else
    { success: false, error: "railway variables command failed (exit code: #{$?.exitstatus})" }
  end
end

.set_one_with_retry(env, service_name, key, value, raw_value: false) ⇒ Hash

Set a single variable with retry logic for transient network errors.

Returns:

  • (Hash)

    { success: true } or { success: false, error: String }



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb', line 123

def self.set_one_with_retry(env, service_name, key, value, raw_value: false)
  last_error = nil

  MAX_RETRIES.times do |attempt|
    result = set_one(env, service_name, key, value, raw_value: raw_value)
    return result if result[:success]

    last_error = result[:error]
    # Only retry on connection/SSL errors
    break unless last_error.to_s =~ /connection|ssl|reset|timeout|network/i

    puts "  ⚠️  Retrying #{key} (attempt #{attempt + 2}/#{MAX_RETRIES})..." if attempt < MAX_RETRIES - 1
    sleep RETRY_DELAY
  end

  { success: false, error: last_error }
end