@@ -20,6 +20,21 @@ require_pr_number() {
2020 fi
2121}
2222
23+ sleep_with_jitter () {
24+ local sleep_seconds=" $1 "
25+ local jittered_sleep=" "
26+
27+ jittered_sleep=" $(
28+ python3 - << PY
29+ import random
30+ base = float(${sleep_seconds} )
31+ print(base * (0.5 + random.random()))
32+ PY
33+ ) "
34+
35+ sleep " ${jittered_sleep} "
36+ }
37+
2338pr_env_name () {
2439 local pr_number=" $1 "
2540
@@ -44,6 +59,57 @@ railway_env_exists_count() {
4459 " ${output_path} "
4560}
4661
62+ railway_link_service () {
63+ local project_id=" $1 "
64+ local service=" $2 "
65+ local env_name=" $3 "
66+
67+ if ! railway link \
68+ --project " ${project_id} " \
69+ --service " ${service} " \
70+ --environment " ${env_name} " \
71+ > /dev/null; then
72+ echo " Failed to link Railway CLI to project ${project_id} service ${service} env ${env_name} ." >&2
73+ return 1
74+ fi
75+ }
76+
77+ railway_extract_runtime_var () {
78+ local service=" $1 "
79+ local env_name=" $2 "
80+ local key=" $3 "
81+ local max_attempts=" ${4:- 20} "
82+ local sleep_seconds=" ${5:- 2} "
83+ local attempt=" "
84+ local value=" "
85+
86+ for attempt in $( seq 1 " ${max_attempts} " ) ; do
87+ value=" $(
88+ railway variable list \
89+ --service " ${service} " \
90+ --environment " ${env_name} " \
91+ --json |
92+ jq -r --arg key " ${key} " ' .[$key] // empty'
93+ ) "
94+
95+ if [ -n " ${value} " ] && ! printf ' %s' " ${value} " | grep -q ' \$[{][{]' ; then
96+ printf ' %s' " ${value} "
97+ return 0
98+ fi
99+
100+ if [ " ${attempt} " -lt " ${max_attempts} " ]; then
101+ sleep_with_jitter " ${sleep_seconds} "
102+ fi
103+ done
104+
105+ if [ -z " ${value:- } " ]; then
106+ echo " Missing runtime variable ${key} in Railway service ${service} for env ${env_name} ." >&2
107+ else
108+ echo " Runtime variable ${key} is unresolved (${value} ) after waiting for Railway interpolation." >&2
109+ fi
110+ return 1
111+ }
112+
47113mask_env_vars () {
48114 local var_name
49115 for var_name in " $@ " ; do
@@ -53,9 +119,168 @@ mask_env_vars() {
53119 done
54120}
55121
122+ railway_graphql () {
123+ local query=" $1 "
124+ local payload=" "
125+
126+ payload=" $( jq -nc --arg query " ${query} " ' {query: $query}' ) "
127+
128+ curl --connect-timeout 10 --max-time 30 -fsS \
129+ -H " Content-Type: application/json" \
130+ -H " Authorization: Bearer ${RAILWAY_API_TOKEN} " \
131+ -H " User-Agent: Mozilla/5.0" \
132+ -H " Origin: https://railway.com" \
133+ -H " Referer: https://railway.com/" \
134+ -d " ${payload} " \
135+ https://backboard.railway.com/graphql/v2
136+ }
137+
138+ railway_environment_id () {
139+ local project_id=" $1 "
140+ local env_name=" $2 "
141+ local response=" "
142+
143+ response=" $(
144+ railway_graphql " $( cat << EOF
145+ query {
146+ environments(projectId: "${project_id} ") {
147+ edges {
148+ node {
149+ id
150+ name
151+ }
152+ }
153+ }
154+ }
155+ EOF
156+ ) "
157+ ) "
158+
159+ jq -r --arg env_name " ${env_name} " ' .data.environments.edges[] | select(.node.name == $env_name) | .node.id' <<< " ${response}"
160+ }
161+
162+ railway_service_id_for_env () {
163+ local env_id=" $1 "
164+ local service_name=" $2 "
165+ local response=" "
166+
167+ response=" $(
168+ railway_graphql " $( cat << EOF
169+ query {
170+ environment(id: "${env_id} ") {
171+ serviceInstances {
172+ edges {
173+ node {
174+ serviceId
175+ serviceName
176+ }
177+ }
178+ }
179+ }
180+ }
181+ EOF
182+ ) "
183+ ) "
184+
185+ jq -r --arg service_name " ${service_name} " ' .data.environment.serviceInstances.edges[] | select(.node.serviceName == $service_name) | .node.serviceId' <<< " ${response}"
186+ }
187+
188+ railway_ensure_tcp_proxy () {
189+ local project_id=" $1 "
190+ local env_name=" $2 "
191+ local service_name=" $3 "
192+ local application_port=" $4 "
193+ local max_attempts=" ${5:- 30} "
194+ local sleep_seconds=" ${6:- 2} "
195+ local env_id=" "
196+ local service_id=" "
197+ local response=" "
198+ local count=" "
199+ local active=" "
200+ local attempt=" "
201+
202+ env_id=" $( railway_environment_id " ${project_id} " " ${env_name} " ) "
203+ if [ -z " ${env_id} " ]; then
204+ echo " Unable to resolve Railway environment ID for ${env_name} ." >&2
205+ return 1
206+ fi
207+
208+ service_id=" $( railway_service_id_for_env " ${env_id} " " ${service_name} " ) "
209+ if [ -z " ${service_id} " ]; then
210+ echo " Unable to resolve Railway service ID for ${service_name} in ${env_name} ." >&2
211+ return 1
212+ fi
213+
214+ response=" $(
215+ railway_graphql " $( cat << EOF
216+ query {
217+ tcpProxies(environmentId: "${env_id} ", serviceId: "${service_id} ") {
218+ id
219+ domain
220+ proxyPort
221+ applicationPort
222+ syncStatus
223+ }
224+ }
225+ EOF
226+ ) "
227+ ) "
228+
229+ count=" $( jq -r --argjson application_port " ${application_port} " ' [.data.tcpProxies[] | select(.applicationPort == $application_port)] | length' <<< " ${response}" ) "
230+ if [ " ${count} " = " 0" ]; then
231+ response=" $(
232+ railway_graphql " $( cat << EOF
233+ mutation {
234+ tcpProxyCreate(input: {
235+ environmentId: "${env_id} "
236+ serviceId: "${service_id} "
237+ applicationPort: ${application_port}
238+ }) {
239+ id
240+ }
241+ }
242+ EOF
243+ ) "
244+ ) "
245+
246+ if echo " ${response} " | jq -e ' .errors' > /dev/null 2>&1 ; then
247+ echo " Failed to create TCP proxy for ${service_name} in ${env_name} : $( echo " ${response} " | jq -r ' .errors[0].message // "unknown error"' ) " >&2
248+ return 1
249+ fi
250+ fi
251+
252+ for attempt in $( seq 1 " ${max_attempts} " ) ; do
253+ response=" $(
254+ railway_graphql " $( cat << EOF
255+ query {
256+ tcpProxies(environmentId: "${env_id} ", serviceId: "${service_id} ") {
257+ applicationPort
258+ syncStatus
259+ }
260+ }
261+ EOF
262+ ) "
263+ ) "
264+
265+ active=" $( jq -r --argjson application_port " ${application_port} " ' [.data.tcpProxies[] | select(.applicationPort == $application_port and .syncStatus == "ACTIVE")] | length' <<< " ${response}" ) "
266+ if [ " ${active} " != " 0" ]; then
267+ return 0
268+ fi
269+
270+ if [ " ${attempt} " -lt " ${max_attempts} " ]; then
271+ sleep_with_jitter " ${sleep_seconds} "
272+ fi
273+ done
274+
275+ echo " TCP proxy for ${service_name} in ${env_name} did not become ACTIVE." >&2
276+ return 1
277+ }
278+
56279redact_preview_logs () {
57280 sed -E \
58281 -e ' s#(postgres(ql)?://)[^[:space:]]+#\1[REDACTED]#g' \
59282 -e ' s#([A-Z_]*(SECRET|KEY|TOKEN|PASSWORD)[A-Z_]*[:=])[^\r\n[:space:]]+#\1[REDACTED]#g' \
283+ -e ' s#((s|S)et-(c|C)ookie:[[:space:]]*better-auth[^=]*=)[^;[:space:]]+#\1[REDACTED]#g' \
284+ -e ' s#(better-auth\.[^=]+=)[^;[:space:]]+#\1[REDACTED]#g' \
60285 -e ' s#(Bearer )[A-Za-z0-9._-]+#\1[REDACTED]#g'
61286}
0 commit comments