Skip to content

Commit 0fa612c

Browse files
vnv-varunamikofalvy
authored andcommitted
ci: seed preview auth in PR previews (#2775)
* ci: bootstrap preview auth * ci: require secure preview auth config * ci: recover preview auth runtime vars * ci: install railway in preview bootstrap * ci: provision preview db tcp proxies * ci: proxy preview spicedb bootstrap * ci: harden preview retry and error logging --------- Co-authored-by: Andrew Mikofalvy <5668128+amikofalvy@users.noreply.github.com>
1 parent cc08989 commit 0fa612c

File tree

7 files changed

+527
-62
lines changed

7 files changed

+527
-62
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
# shellcheck source=.github/scripts/preview/common.sh
6+
source "${SCRIPT_DIR}/common.sh"
7+
8+
require_env_vars \
9+
API_URL \
10+
SPICEDB_PRESHARED_KEY \
11+
INKEEP_AGENTS_MANAGE_UI_USERNAME \
12+
INKEEP_AGENTS_MANAGE_UI_PASSWORD \
13+
BETTER_AUTH_SECRET
14+
15+
mask_env_vars RUN_DB_URL SPICEDB_ENDPOINT SPICEDB_PRESHARED_KEY INKEEP_AGENTS_MANAGE_UI_PASSWORD BETTER_AUTH_SECRET
16+
17+
if [ -z "${RUN_DB_URL:-}" ] || [ -z "${SPICEDB_ENDPOINT:-}" ]; then
18+
require_env_vars \
19+
RAILWAY_API_TOKEN \
20+
RAILWAY_PROJECT_ID \
21+
RAILWAY_OUTPUT_SERVICE \
22+
RAILWAY_RUN_DB_URL_KEY \
23+
RAILWAY_SPICEDB_ENDPOINT_KEY \
24+
PR_NUMBER
25+
26+
RAILWAY_ENV_NAME="$(pr_env_name "${PR_NUMBER}")"
27+
28+
railway_link_service "${RAILWAY_PROJECT_ID}" "${RAILWAY_OUTPUT_SERVICE}" "${RAILWAY_ENV_NAME}"
29+
30+
if [ -z "${RUN_DB_URL:-}" ]; then
31+
RUN_DB_URL="$(railway_extract_runtime_var "${RAILWAY_OUTPUT_SERVICE}" "${RAILWAY_ENV_NAME}" "${RAILWAY_RUN_DB_URL_KEY}")"
32+
fi
33+
34+
if [ -z "${SPICEDB_ENDPOINT:-}" ]; then
35+
SPICEDB_ENDPOINT="$(railway_extract_runtime_var "${RAILWAY_OUTPUT_SERVICE}" "${RAILWAY_ENV_NAME}" "${RAILWAY_SPICEDB_ENDPOINT_KEY}")"
36+
fi
37+
38+
mask_env_vars RUN_DB_URL SPICEDB_ENDPOINT
39+
fi
40+
41+
require_env_vars RUN_DB_URL SPICEDB_ENDPOINT
42+
43+
export INKEEP_AGENTS_API_URL="${API_URL}"
44+
export INKEEP_AGENTS_RUN_DATABASE_URL="${RUN_DB_URL}"
45+
export SPICEDB_ENDPOINT
46+
export TENANT_ID="${TENANT_ID:-default}"
47+
48+
echo "::group::Run preview runtime migrations"
49+
pnpm db:run:migrate
50+
echo "::endgroup::"
51+
52+
echo "::group::Initialize preview auth"
53+
pnpm db:auth:init
54+
echo "::endgroup::"
55+
56+
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
57+
{
58+
echo "## Preview Auth Bootstrap"
59+
echo "- Tenant: \`${TENANT_ID}\`"
60+
echo "- Admin email: \`${INKEEP_AGENTS_MANAGE_UI_USERNAME}\`"
61+
echo "- Runtime migrations: \`pnpm db:run:migrate\`"
62+
echo "- Auth seed: \`pnpm db:auth:init\`"
63+
} >> "${GITHUB_STEP_SUMMARY}"
64+
fi

.github/scripts/preview/capture-preview-failure-diagnostics.sh

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,48 @@ echo "## Smoke Failure Diagnostics" >> "${GITHUB_STEP_SUMMARY}"
1818
echo
1919
echo "### UI response"
2020
curl --connect-timeout 5 --max-time 15 -I -sS "${UI_URL}" || true
21-
} | tee /tmp/preview-smoke-diagnostics.txt
21+
echo
22+
if [ -n "${INKEEP_AGENTS_MANAGE_UI_USERNAME:-}" ] && [ -n "${INKEEP_AGENTS_MANAGE_UI_PASSWORD:-}" ]; then
23+
tenant_id="${TENANT_ID:-default}"
24+
tmpdir="$(mktemp -d)"
25+
trap 'rm -rf "${tmpdir}"' EXIT
26+
27+
cookie_jar="${tmpdir}/cookies.txt"
28+
sign_in_headers="${tmpdir}/sign-in-headers.txt"
29+
sign_in_body="${tmpdir}/sign-in-body.txt"
30+
manage_body="${tmpdir}/manage-projects-body.txt"
31+
32+
sign_in_status="$(
33+
curl --connect-timeout 5 --max-time 20 -sS \
34+
-c "${cookie_jar}" \
35+
-D "${sign_in_headers}" \
36+
-o "${sign_in_body}" \
37+
-w '%{http_code}' \
38+
-H 'Content-Type: application/json' \
39+
-H "Origin: ${UI_URL}" \
40+
-d "$(jq -cn \
41+
--arg email "${INKEEP_AGENTS_MANAGE_UI_USERNAME}" \
42+
--arg password "${INKEEP_AGENTS_MANAGE_UI_PASSWORD}" \
43+
'{email:$email, password:$password}')" \
44+
"${API_URL}/api/auth/sign-in/email" || true
45+
)"
46+
47+
echo "### API sign-in response (${sign_in_status})"
48+
cat "${sign_in_headers}"
49+
cat "${sign_in_body}"
50+
echo
51+
52+
manage_status="$(
53+
curl --connect-timeout 5 --max-time 20 -sS \
54+
-b "${cookie_jar}" \
55+
-o "${manage_body}" \
56+
-w '%{http_code}' \
57+
-H 'Accept: application/json' \
58+
"${API_URL}/manage/tenants/${tenant_id}/projects" || true
59+
)"
60+
61+
echo "### Authenticated manage/projects response (${manage_status})"
62+
cat "${manage_body}"
63+
echo
64+
fi
65+
} | redact_preview_logs | tee /tmp/preview-smoke-diagnostics.txt

.github/scripts/preview/common.sh

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
2338
pr_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+
47113
mask_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+
56279
redact_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

Comments
 (0)