openclacky 0.9.28 → 0.9.30
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 +39 -0
- data/docs/deploy-architecture.md +619 -0
- data/lib/clacky/agent/llm_caller.rb +14 -2
- data/lib/clacky/agent/message_compressor.rb +24 -6
- data/lib/clacky/agent/message_compressor_helper.rb +17 -10
- data/lib/clacky/agent/session_serializer.rb +69 -0
- data/lib/clacky/agent/skill_manager.rb +2 -2
- data/lib/clacky/agent.rb +3 -0
- data/lib/clacky/brand_config.rb +29 -3
- data/lib/clacky/clacky_auth_client.rb +152 -0
- data/lib/clacky/clacky_cloud_config.rb +123 -0
- data/lib/clacky/cli.rb +13 -0
- data/lib/clacky/client.rb +21 -7
- data/lib/clacky/cloud_project_client.rb +169 -0
- data/lib/clacky/default_agents/base_prompt.md +1 -0
- data/lib/clacky/default_parsers/doc_parser.rb +9 -9
- data/lib/clacky/default_skills/browser-setup/SKILL.md +9 -0
- data/lib/clacky/default_skills/channel-setup/SKILL.md +21 -4
- data/lib/clacky/default_skills/channel-setup/feishu_setup.rb +8 -2
- data/lib/clacky/default_skills/deploy/SKILL.md +96 -5
- data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +1268 -274
- data/lib/clacky/default_skills/deploy/tools/create_database_service.rb +341 -0
- data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +72 -147
- data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +60 -50
- data/lib/clacky/default_skills/deploy/tools/list_services.rb +47 -60
- data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +147 -96
- data/lib/clacky/default_skills/new/SKILL.md +117 -5
- data/lib/clacky/default_skills/new/scripts/cloud_project_init.sh +74 -0
- data/lib/clacky/default_skills/new/scripts/create_rails_project.sh +32 -0
- data/lib/clacky/deploy_api_client.rb +484 -0
- data/lib/clacky/json_ui_controller.rb +16 -10
- data/lib/clacky/message_format/bedrock.rb +3 -2
- data/lib/clacky/message_history.rb +8 -0
- data/lib/clacky/plain_ui_controller.rb +1 -6
- data/lib/clacky/providers.rb +23 -4
- data/lib/clacky/server/browser_manager.rb +3 -1
- data/lib/clacky/server/channel/adapters/feishu/ws_client.rb +2 -1
- data/lib/clacky/server/channel/adapters/wecom/ws_client.rb +3 -1
- data/lib/clacky/server/channel/adapters/weixin/adapter.rb +5 -5
- data/lib/clacky/server/http_server.rb +12 -2
- data/lib/clacky/server/server_master.rb +43 -7
- data/lib/clacky/server/web_ui_controller.rb +17 -9
- data/lib/clacky/skill.rb +6 -2
- data/lib/clacky/tools/run_project.rb +4 -1
- data/lib/clacky/tools/shell.rb +7 -1
- data/lib/clacky/ui2/ui_controller.rb +1 -5
- data/lib/clacky/ui_interface.rb +5 -7
- data/lib/clacky/utils/arguments_parser.rb +22 -5
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +45 -5
- data/lib/clacky/web/app.js +126 -19
- data/lib/clacky/web/i18n.js +57 -0
- data/lib/clacky/web/sessions.js +108 -39
- data/lib/clacky/web/skills.js +8 -2
- data/lib/clacky.rb +3 -0
- metadata +8 -1
|
@@ -1,66 +1,76 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
3
7
|
module Clacky
|
|
4
8
|
module DeployTools
|
|
5
|
-
# Fetch runtime logs from deployed service
|
|
9
|
+
# Fetch runtime (environment) logs from a deployed Railway service via
|
|
10
|
+
# the Clacky Deploy API SSE endpoint.
|
|
11
|
+
#
|
|
12
|
+
# Uses DeployApiClient#stream_build_logs for build-phase logs (on failure),
|
|
13
|
+
# and this class for runtime log fetching (e.g. post-deploy diagnostics).
|
|
6
14
|
class FetchRuntimeLogs
|
|
7
|
-
DEFAULT_LINES
|
|
8
|
-
MAX_LINES
|
|
15
|
+
DEFAULT_LINES = 50
|
|
16
|
+
MAX_LINES = 200
|
|
17
|
+
STREAM_TIMEOUT = 60 # seconds
|
|
9
18
|
|
|
10
|
-
# Fetch runtime logs
|
|
19
|
+
# Fetch recent runtime logs for a project.
|
|
11
20
|
#
|
|
12
|
-
# @param
|
|
13
|
-
# @param
|
|
14
|
-
# @
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
# @param project_id [String] Clacky project ID
|
|
22
|
+
# @param workspace_key [String] clacky_ak_* key
|
|
23
|
+
# @param base_url [String] API base URL
|
|
24
|
+
# @param lines [Integer] max log lines to collect
|
|
25
|
+
# @param keyword [String] optional filter keyword
|
|
26
|
+
# @return [Hash] { success: true, logs: Array<String> }
|
|
27
|
+
# or { success: false, error: String }
|
|
28
|
+
def self.execute(project_id:, workspace_key:, base_url:,
|
|
29
|
+
lines: DEFAULT_LINES, keyword: nil)
|
|
30
|
+
lines = [[lines.to_i, 1].max, MAX_LINES].min
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if lines <= 0 || lines > MAX_LINES
|
|
26
|
-
return {
|
|
27
|
-
error: "Invalid lines parameter",
|
|
28
|
-
details: "Lines must be between 1 and #{MAX_LINES}",
|
|
29
|
-
provided: lines
|
|
30
|
-
}
|
|
31
|
-
end
|
|
32
|
+
params = "project_id=#{URI.encode_www_form_component(project_id)}"
|
|
33
|
+
params += "&keyword=#{URI.encode_www_form_component(keyword)}" if keyword
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
uri = URI.parse(
|
|
36
|
+
"#{base_url.to_s.sub(%r{/+$}, "")}" \
|
|
37
|
+
"/openclacky/v1/deploy/logs/environment/stream?#{params}"
|
|
38
|
+
)
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
command = "clackycli logs -s #{shell_escape(service_name)} --lines #{lines}"
|
|
37
|
-
output = `#{command} 2>&1`
|
|
38
|
-
exit_code = $?.exitstatus
|
|
40
|
+
collected = []
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
exit_code: exit_code,
|
|
45
|
-
service: service_name
|
|
46
|
-
}
|
|
47
|
-
end
|
|
42
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
43
|
+
http.use_ssl = (uri.scheme == "https")
|
|
44
|
+
http.open_timeout = 10
|
|
45
|
+
http.read_timeout = STREAM_TIMEOUT
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
lines_requested: lines,
|
|
53
|
-
logs: output,
|
|
54
|
-
timestamp: Time.now.iso8601
|
|
55
|
-
}
|
|
56
|
-
end
|
|
47
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
|
48
|
+
req["Authorization"] = "Bearer #{workspace_key}"
|
|
49
|
+
req["Accept"] = "text/event-stream"
|
|
57
50
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
51
|
+
http.request(req) do |response|
|
|
52
|
+
response.read_body do |chunk|
|
|
53
|
+
chunk.split("\n").each do |raw|
|
|
54
|
+
next unless raw.start_with?("data:")
|
|
55
|
+
|
|
56
|
+
json_str = raw.sub(/\Adata:\s*/, "")
|
|
57
|
+
next if json_str.strip.empty?
|
|
58
|
+
|
|
59
|
+
begin
|
|
60
|
+
event = JSON.parse(json_str)
|
|
61
|
+
msg = event["message"].to_s
|
|
62
|
+
collected << msg unless msg.empty?
|
|
63
|
+
return { success: true, logs: collected } if collected.size >= lines
|
|
64
|
+
rescue JSON::ParserError
|
|
65
|
+
# skip malformed events
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
{ success: true, logs: collected }
|
|
72
|
+
rescue => e
|
|
73
|
+
{ success: false, error: "Failed to fetch runtime logs: #{e.message}" }
|
|
64
74
|
end
|
|
65
75
|
end
|
|
66
76
|
end
|
|
@@ -1,79 +1,66 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "open3"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
3
6
|
module Clacky
|
|
4
7
|
module DeployTools
|
|
5
|
-
# List Railway services
|
|
8
|
+
# List Railway services for the linked project.
|
|
9
|
+
# Uses RAILWAY_TOKEN passed through environment — no clackycli wrapper needed.
|
|
10
|
+
#
|
|
11
|
+
# NOTE: In the new deploy flow, service discovery is primarily done via the
|
|
12
|
+
# Clacky Deploy API (deploy/services endpoint). This tool is kept as a
|
|
13
|
+
# fallback for detecting the main service name via `railway service list`.
|
|
6
14
|
class ListServices
|
|
7
|
-
SENSITIVE_PATTERNS = [
|
|
8
|
-
/password/i,
|
|
9
|
-
/secret/i,
|
|
10
|
-
/api_key/i,
|
|
11
|
-
/token/i,
|
|
12
|
-
/credential/i,
|
|
13
|
-
/private_key/i
|
|
14
|
-
].freeze
|
|
15
15
|
|
|
16
|
-
#
|
|
16
|
+
# List services for the current Railway project.
|
|
17
17
|
#
|
|
18
|
-
# @
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
# @param platform_token [String] RAILWAY_TOKEN for this deploy task
|
|
19
|
+
# @return [Hash] {
|
|
20
|
+
# success: Boolean,
|
|
21
|
+
# services: Array<Hash>,
|
|
22
|
+
# main_service: Hash | nil, # first non-middleware service
|
|
23
|
+
# db_service: Hash | nil # first postgres/mysql service
|
|
24
|
+
# }
|
|
25
|
+
def self.execute(platform_token:)
|
|
26
|
+
if platform_token.nil? || platform_token.strip.empty?
|
|
27
|
+
return { success: false, error: "platform_token is required" }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
env = ENV.to_h.merge("RAILWAY_TOKEN" => platform_token)
|
|
31
|
+
out, err, status = Open3.capture3(env, "railway status --json")
|
|
22
32
|
|
|
23
|
-
|
|
33
|
+
unless status.success?
|
|
24
34
|
return {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
success: false,
|
|
36
|
+
error: "railway service list failed (exit #{status.exitstatus})",
|
|
37
|
+
details: err
|
|
28
38
|
}
|
|
29
39
|
end
|
|
30
40
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
masked_services = mask_sensitive_data(services)
|
|
34
|
-
|
|
35
|
-
{
|
|
36
|
-
success: true,
|
|
37
|
-
services: masked_services,
|
|
38
|
-
count: masked_services.length
|
|
39
|
-
}
|
|
40
|
-
rescue JSON::ParserError => e
|
|
41
|
-
{
|
|
42
|
-
error: "Failed to parse Railway CLI output",
|
|
43
|
-
details: e.message,
|
|
44
|
-
raw_output: output
|
|
45
|
-
}
|
|
46
|
-
end
|
|
47
|
-
end
|
|
41
|
+
info = JSON.parse(out)
|
|
42
|
+
services = info["services"] || []
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
# @return [Array<Hash>] Services with masked sensitive data
|
|
53
|
-
def self.mask_sensitive_data(services)
|
|
54
|
-
services.map do |service|
|
|
55
|
-
service = service.dup
|
|
56
|
-
|
|
57
|
-
if service['variables']
|
|
58
|
-
service['variables'] = mask_variables(service['variables'])
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
service
|
|
44
|
+
main_svc = services.find do |s|
|
|
45
|
+
name = s["name"].to_s.downcase
|
|
46
|
+
!%w[postgres postgresql mysql redis].any? { |db| name.include?(db) }
|
|
62
47
|
end
|
|
63
|
-
end
|
|
64
48
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# @return [Hash] Variables with sensitive values masked
|
|
69
|
-
def self.mask_variables(variables)
|
|
70
|
-
variables.transform_values do |value|
|
|
71
|
-
next value unless value.is_a?(String)
|
|
72
|
-
|
|
73
|
-
# Check if variable name matches sensitive patterns
|
|
74
|
-
is_sensitive = SENSITIVE_PATTERNS.any? { |pattern| value =~ pattern }
|
|
75
|
-
is_sensitive ? '******' : value
|
|
49
|
+
db_svc = services.find do |s|
|
|
50
|
+
name = s["name"].to_s.downcase
|
|
51
|
+
%w[postgres postgresql mysql].any? { |db| name.include?(db) }
|
|
76
52
|
end
|
|
53
|
+
|
|
54
|
+
{
|
|
55
|
+
success: true,
|
|
56
|
+
services: services,
|
|
57
|
+
main_service: main_svc,
|
|
58
|
+
db_service: db_svc
|
|
59
|
+
}
|
|
60
|
+
rescue JSON::ParserError => e
|
|
61
|
+
{ success: false, error: "Failed to parse service list: #{e.message}", raw: out.to_s[0, 200] }
|
|
62
|
+
rescue => e
|
|
63
|
+
{ success: false, error: "Unexpected error: #{e.message}" }
|
|
77
64
|
end
|
|
78
65
|
end
|
|
79
66
|
end
|
|
@@ -1,137 +1,188 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
3
5
|
module Clacky
|
|
4
6
|
module DeployTools
|
|
5
|
-
# Set environment variables
|
|
7
|
+
# Set environment variables on a Railway service via `railway variables --set`.
|
|
8
|
+
# Uses RAILWAY_TOKEN passed through environment — no clackycli wrapper needed.
|
|
9
|
+
#
|
|
10
|
+
# Supports both normal key=value pairs and Railway inter-service references
|
|
11
|
+
# like ${{postgres.DATABASE_PUBLIC_URL}} (pass raw_value: true to skip escaping).
|
|
6
12
|
class SetDeployVariables
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
|
|
9
14
|
SENSITIVE_PATTERNS = [
|
|
10
|
-
/password/i,
|
|
11
|
-
/
|
|
12
|
-
/api_key/i,
|
|
13
|
-
/token/i,
|
|
14
|
-
/credential/i,
|
|
15
|
-
/private_key/i
|
|
15
|
+
/password/i, /secret/i, /api_key/i,
|
|
16
|
+
/token/i, /credential/i, /private_key/i
|
|
16
17
|
].freeze
|
|
17
18
|
|
|
18
|
-
#
|
|
19
|
+
# Maximum number of variables to set in a single batch call
|
|
20
|
+
BATCH_SIZE = 20
|
|
21
|
+
|
|
22
|
+
# Retry config for transient failures
|
|
23
|
+
MAX_RETRIES = 3
|
|
24
|
+
RETRY_DELAY = 2 # seconds
|
|
25
|
+
|
|
26
|
+
# Set one or more environment variables on a Railway service.
|
|
27
|
+
# Batches all variables into a single `railway variables` call to minimize
|
|
28
|
+
# network connections and avoid SSL reset issues.
|
|
19
29
|
#
|
|
20
|
-
# @param service_name
|
|
21
|
-
# @param variables
|
|
22
|
-
# @param
|
|
23
|
-
# @
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
# @param service_name [String] Railway service name
|
|
31
|
+
# @param variables [Hash] KEY => VALUE pairs
|
|
32
|
+
# @param platform_token [String] RAILWAY_TOKEN for this deploy task
|
|
33
|
+
# @param raw_value [Boolean] when true, values are passed unquoted
|
|
34
|
+
# (for Railway ${{...}} references)
|
|
35
|
+
# @return [Hash] {
|
|
36
|
+
# success: Boolean,
|
|
37
|
+
# set_variables: Array<String>,
|
|
38
|
+
# errors: Array<Hash>
|
|
39
|
+
# }
|
|
40
|
+
def self.execute(service_name:, variables:, platform_token:, raw_value: false)
|
|
41
|
+
if service_name.nil? || service_name.strip.empty?
|
|
42
|
+
return { success: false, error: "service_name is required" }
|
|
31
43
|
end
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
success: true,
|
|
35
|
-
service: service_name,
|
|
36
|
-
set_variables: [],
|
|
37
|
-
skipped_variables: [],
|
|
38
|
-
errors: []
|
|
39
|
-
}
|
|
45
|
+
env = ENV.to_h.merge("RAILWAY_TOKEN" => platform_token)
|
|
40
46
|
|
|
41
|
-
#
|
|
47
|
+
# Log all variables being set
|
|
42
48
|
variables.each do |key, value|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
results[:set_variables] << { key: key, type: 'simple' }
|
|
46
|
-
elsif result[:skipped]
|
|
47
|
-
results[:skipped_variables] << { key: key, reason: result[:reason] }
|
|
48
|
-
else
|
|
49
|
-
results[:errors] << { key: key, error: result[:error] }
|
|
50
|
-
end
|
|
49
|
+
log_value = sensitive?(key) ? "******" : value
|
|
50
|
+
puts " Setting #{key}=#{log_value}"
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
# Split into batches to avoid command line length limits
|
|
54
|
+
set_vars = []
|
|
55
|
+
error_list = []
|
|
56
|
+
var_pairs = variables.map { |k, v| [k.to_s, v.to_s] }
|
|
57
|
+
|
|
58
|
+
var_pairs.each_slice(BATCH_SIZE) do |batch|
|
|
59
|
+
result = set_batch(env, service_name, batch, raw_value: raw_value)
|
|
56
60
|
if result[:success]
|
|
57
|
-
|
|
58
|
-
elsif result[:skipped]
|
|
59
|
-
results[:skipped_variables] << { key: key, reason: result[:reason] }
|
|
61
|
+
set_vars.concat(batch.map(&:first))
|
|
60
62
|
else
|
|
61
|
-
|
|
63
|
+
# Retry logic: attempt individual vars if batch fails
|
|
64
|
+
batch.each do |key, value|
|
|
65
|
+
individual = set_one_with_retry(env, service_name, key, value, raw_value: raw_value)
|
|
66
|
+
if individual[:success]
|
|
67
|
+
set_vars << key
|
|
68
|
+
else
|
|
69
|
+
error_list << { key: key, error: individual[:error] }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
62
72
|
end
|
|
63
73
|
end
|
|
64
74
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
{
|
|
76
|
+
success: error_list.empty?,
|
|
77
|
+
set_variables: set_vars,
|
|
78
|
+
errors: error_list
|
|
79
|
+
}
|
|
68
80
|
end
|
|
69
81
|
|
|
70
|
-
# Set a single
|
|
82
|
+
# Set a batch of variables in a single railway command call.
|
|
71
83
|
#
|
|
72
|
-
# @param
|
|
73
|
-
# @param
|
|
74
|
-
# @param
|
|
75
|
-
# @
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
# @param env [Hash] environment variables
|
|
85
|
+
# @param service_name [String] Railway service name
|
|
86
|
+
# @param pairs [Array<Array>] [[key, value], ...]
|
|
87
|
+
# @return [Hash] { success: true } or { success: false, error: String }
|
|
88
|
+
def self.set_batch(env, service_name, pairs, raw_value: false)
|
|
89
|
+
# Each --set argument is passed as a separate array element
|
|
90
|
+
set_flags = pairs.flat_map { |key, value| ["--set", "#{key}=#{value}"] }
|
|
91
|
+
cmd = ["railway", "variables", "--service", service_name, "--skip-deploys"] + set_flags
|
|
92
|
+
|
|
93
|
+
# Debug: print the full command being executed
|
|
94
|
+
puts " [DEBUG] Executing Railway CLI command:"
|
|
95
|
+
puts " [DEBUG] Array form: #{cmd.inspect}"
|
|
96
|
+
puts " [DEBUG] Shell form: #{cmd.join(' ')}"
|
|
97
|
+
puts " [DEBUG] with RAILWAY_TOKEN=#{env['RAILWAY_TOKEN']}" if env['RAILWAY_TOKEN']
|
|
98
|
+
$stdout.flush
|
|
99
|
+
|
|
100
|
+
# Use system() instead of Open3.capture3 to avoid stdin/stdout blocking issues
|
|
101
|
+
# system() inherits the current process's stdin/stdout/stderr directly
|
|
102
|
+
require 'timeout'
|
|
103
|
+
|
|
104
|
+
begin
|
|
105
|
+
success = Timeout.timeout(30) do
|
|
106
|
+
# Close stdin, suppress stdout, but keep stderr visible
|
|
107
|
+
system(env, *cmd, in: :close, out: File::NULL)
|
|
108
|
+
end
|
|
109
|
+
rescue Timeout::Error
|
|
110
|
+
return { success: false, error: "Railway CLI command timed out after 30 seconds" }
|
|
85
111
|
end
|
|
86
112
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
command = "clackycli variables -s #{shell_escape(service_name)} --set-ref #{shell_escape(key)}=#{shell_escape(value)}"
|
|
113
|
+
if success
|
|
114
|
+
{ success: true }
|
|
90
115
|
else
|
|
91
|
-
|
|
116
|
+
{ success: false, error: "railway variables command failed (exit code: #{$?.exitstatus})" }
|
|
92
117
|
end
|
|
118
|
+
end
|
|
93
119
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
120
|
+
# Set a single variable with retry logic for transient network errors.
|
|
121
|
+
#
|
|
122
|
+
# @return [Hash] { success: true } or { success: false, error: String }
|
|
123
|
+
def self.set_one_with_retry(env, service_name, key, value, raw_value: false)
|
|
124
|
+
last_error = nil
|
|
97
125
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
126
|
+
MAX_RETRIES.times do |attempt|
|
|
127
|
+
result = set_one(env, service_name, key, value, raw_value: raw_value)
|
|
128
|
+
return result if result[:success]
|
|
101
129
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
error: output.strip
|
|
109
|
-
}
|
|
130
|
+
last_error = result[:error]
|
|
131
|
+
# Only retry on connection/SSL errors
|
|
132
|
+
break unless last_error.to_s =~ /connection|ssl|reset|timeout|network/i
|
|
133
|
+
|
|
134
|
+
puts " ⚠️ Retrying #{key} (attempt #{attempt + 2}/#{MAX_RETRIES})..." if attempt < MAX_RETRIES - 1
|
|
135
|
+
sleep RETRY_DELAY
|
|
110
136
|
end
|
|
137
|
+
|
|
138
|
+
{ success: false, error: last_error }
|
|
111
139
|
end
|
|
112
140
|
|
|
113
|
-
#
|
|
141
|
+
# Set a single variable. Builds the `railway variables --set` command.
|
|
114
142
|
#
|
|
115
|
-
# @
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
143
|
+
# @return [Hash] { success: true } or { success: false, error: String }
|
|
144
|
+
def self.set_one(env, service_name, key, value, raw_value: false)
|
|
145
|
+
assignment = "#{key}=#{value}"
|
|
146
|
+
|
|
147
|
+
cmd = [
|
|
148
|
+
"railway", "variables",
|
|
149
|
+
"--service", service_name,
|
|
150
|
+
"--skip-deploys",
|
|
151
|
+
"--set", assignment
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
# Debug: print the full command being executed
|
|
155
|
+
puts " [DEBUG] Executing single var Railway CLI command:"
|
|
156
|
+
puts " [DEBUG] Array form: #{cmd.inspect}"
|
|
157
|
+
puts " [DEBUG] Shell form: #{cmd.join(' ')}"
|
|
158
|
+
puts " [DEBUG] with RAILWAY_TOKEN=#{env['RAILWAY_TOKEN']}" if env['RAILWAY_TOKEN']
|
|
159
|
+
$stdout.flush
|
|
160
|
+
|
|
161
|
+
# Use system() instead of Open3.capture3 to avoid stdin/stdout blocking issues
|
|
162
|
+
require 'timeout'
|
|
163
|
+
|
|
164
|
+
begin
|
|
165
|
+
success = Timeout.timeout(30) do
|
|
166
|
+
# Close stdin, suppress stdout, but keep stderr visible
|
|
167
|
+
system(env, *cmd, in: :close, out: File::NULL)
|
|
168
|
+
end
|
|
169
|
+
rescue Timeout::Error
|
|
170
|
+
return { success: false, error: "Railway CLI command timed out after 30 seconds" }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
if success
|
|
174
|
+
{ success: true }
|
|
175
|
+
else
|
|
176
|
+
{ success: false, error: "railway variables command failed (exit code: #{$?.exitstatus})" }
|
|
177
|
+
end
|
|
119
178
|
end
|
|
120
179
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
# @param key [String] Variable name
|
|
124
|
-
# @return [Boolean] True if sensitive
|
|
125
|
-
def self.sensitive_variable?(key)
|
|
126
|
-
SENSITIVE_PATTERNS.any? { |pattern| key =~ pattern }
|
|
180
|
+
private_class_method def self.sensitive?(key)
|
|
181
|
+
SENSITIVE_PATTERNS.any? { |pat| key.match?(pat) }
|
|
127
182
|
end
|
|
128
183
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
# @param str [String] String to escape
|
|
132
|
-
# @return [String] Escaped string
|
|
133
|
-
def self.shell_escape(str)
|
|
134
|
-
"'#{str.gsub("'", "'\\\\''")}'"
|
|
184
|
+
private_class_method def self.shell_escape(str)
|
|
185
|
+
"'#{str.to_s.gsub("'", "'\\\\''")}'"
|
|
135
186
|
end
|
|
136
187
|
end
|
|
137
188
|
end
|