@@ -5,6 +5,8 @@ set -euo pipefail
55# shellcheck source=lib.sh
66source " $( dirname " ${BASH_SOURCE[0]} " ) /lib.sh"
77
8+ install_error_trap
9+
810PROJECT_NAME=" ${RAILWAY_PROJECT_NAME:- agenta-oss-railway} "
911ENV_NAME=" ${RAILWAY_ENVIRONMENT_NAME:- staging} "
1012SKIP_UNSETS=" ${CONFIGURE_SKIP_UNSETS:- false} "
@@ -15,6 +17,13 @@ AGENTA_AUTH_KEY="${AGENTA_AUTH_KEY:-replace-me}"
1517AGENTA_CRYPT_KEY=" ${AGENTA_CRYPT_KEY:- replace-me} "
1618POSTGRES_PASSWORD=" ${POSTGRES_PASSWORD:- } "
1719
20+ # Populated by resolve_railway_ids() after `railway link`. Used by the GraphQL
21+ # variableCollectionUpsert path; left empty -> upsert_service_vars falls back
22+ # to the CLI.
23+ RAILWAY_PROJECT_ID=" "
24+ RAILWAY_ENVIRONMENT_ID=" "
25+ RAILWAY_STATUS_JSON=" "
26+
1827resolve_postgres_password () {
1928 if [ -n " $POSTGRES_PASSWORD " ]; then
2029 return 0
@@ -47,10 +56,89 @@ require_railway_auth() {
4756 }
4857}
4958
59+ # resolve_railway_ids: cache the project/environment IDs and status JSON used by
60+ # the GraphQL variableCollectionUpsert path. No-op (CLI fallback) when there is
61+ # no API token or the IDs cannot be resolved.
62+ resolve_railway_ids () {
63+ [ -n " ${RAILWAY_API_TOKEN:- } " ] || return 0
64+ RAILWAY_STATUS_JSON=" $( railway_call status --json 2> /dev/null || true) "
65+ [ -n " $RAILWAY_STATUS_JSON " ] || return 0
66+ RAILWAY_PROJECT_ID=" $( printf ' %s' " $RAILWAY_STATUS_JSON " | jq -r ' .id // empty' 2> /dev/null || true) "
67+ RAILWAY_ENVIRONMENT_ID=" $( printf ' %s' " $RAILWAY_STATUS_JSON " \
68+ | jq -r --arg e " $ENV_NAME " ' [.environments.edges[].node | select(.name==$e) | .id][0] // empty' 2> /dev/null || true) "
69+ if [ -z " $RAILWAY_PROJECT_ID " ] || [ -z " $RAILWAY_ENVIRONMENT_ID " ]; then
70+ printf " Note: could not resolve Railway project/environment IDs; using CLI variable set.\n" >&2
71+ RAILWAY_PROJECT_ID=" "
72+ fi
73+ }
74+
75+ # _service_id <service-name> -> serviceId, from the cached status JSON.
76+ _service_id () {
77+ printf ' %s' " $RAILWAY_STATUS_JSON " | jq -r --arg n " $1 " --arg e " $ENV_NAME " \
78+ ' [.environments.edges[].node | select(.name==$e)
79+ | .serviceInstances.edges[].node | select(.serviceName==$n) | .serviceId][0] // empty' 2> /dev/null || true
80+ }
81+
82+ # _vars_to_json KEY=VALUE ... -> {"KEY":"VALUE",...} (split on the first '=')
83+ _vars_to_json () {
84+ local json=' {}' kv key val
85+ for kv in " $@ " ; do
86+ key=" ${kv%% =* } "
87+ val=" ${kv#* =} "
88+ json=" $( jq -c --arg k " $key " --arg v " $val " ' . + {($k): $v}' <<< " $json" ) "
89+ done
90+ printf ' %s' " $json "
91+ }
92+
93+ # upsert_service_vars <service> KEY=VALUE ...
94+ # Sets all given variables for a service in ONE variableCollectionUpsert
95+ # (skipDeploys) via the GraphQL API — Railway's recommended path, which avoids
96+ # the CLI fanning out to one slow variableUpsert per key. Falls back to the CLI
97+ # when no API token / IDs are available. Reference values like
98+ # ${{Postgres.POSTGRES_PASSWORD}} are stored verbatim and resolved by Railway at
99+ # render time, exactly as with the CLI.
100+ upsert_service_vars () {
101+ local service=" $1 "
102+ shift
103+ [ " $# " -gt 0 ] || return 0
104+
105+ if [ -z " ${RAILWAY_API_TOKEN:- } " ] || [ -z " $RAILWAY_PROJECT_ID " ]; then
106+ railway_call variable set --service " $service " --environment " $ENV_NAME " --skip-deploys " $@ " > /dev/null
107+ return 0
108+ fi
109+
110+ local svc_id
111+ svc_id=" $( _service_id " $service " ) "
112+ if [ -z " $svc_id " ]; then
113+ # Name didn't match the cached status JSON (e.g. unexpected casing). Don't
114+ # hard-fail the deploy where the CLI's --service would have worked; fall
115+ # back to it.
116+ printf " Could not resolve service id for '%s'; falling back to CLI variable set.\n" " $service " >&2
117+ railway_call variable set --service " $service " --environment " $ENV_NAME " --skip-deploys " $@ " > /dev/null
118+ return 0
119+ fi
120+
121+ # replace:false makes the merge intent explicit: configure.sh calls set_vars
122+ # then set_optional_vars for the same service and relies on accumulation.
123+ # (Verified the API already defaults to merge, but pinning it avoids any
124+ # future default change silently wiping earlier variables.)
125+ local vars_json payload
126+ vars_json=" $( _vars_to_json " $@ " ) "
127+ payload=" $( jq -nc \
128+ --arg p " $RAILWAY_PROJECT_ID " \
129+ --arg e " $RAILWAY_ENVIRONMENT_ID " \
130+ --arg s " $svc_id " \
131+ --argjson vars " $vars_json " \
132+ ' {query: "mutation($input: VariableCollectionUpsertInput!){ variableCollectionUpsert(input: $input) }",
133+ variables: {input: {projectId: $p, environmentId: $e, serviceId: $s, skipDeploys: true, replace: false, variables: $vars}}}' ) "
134+
135+ _railway_graphql " $payload " > /dev/null
136+ }
137+
50138set_vars () {
51139 local service=" $1 "
52140 shift
53- railway_call variable set --service " $service " --environment " $ENV_NAME " --skip-deploys " $@ " > /dev/null
141+ upsert_service_vars " $service " " $@ "
54142}
55143
56144set_optional_vars () {
@@ -66,7 +154,7 @@ set_optional_vars() {
66154 done
67155
68156 if [ " ${# args[@]} " -gt 0 ]; then
69- railway_call variable set --service " $service " --environment " $ENV_NAME " --skip-deploys " $ {args[@]}" > /dev/null
157+ upsert_service_vars " $service " " $ {args[@]}"
70158 fi
71159}
72160
@@ -100,6 +188,10 @@ main() {
100188
101189 railway_call link --project " $PROJECT_NAME " --environment " $ENV_NAME " --json > /dev/null
102190
191+ # Resolve IDs for the GraphQL variableCollectionUpsert path (after link, so
192+ # the project/environment/services exist and are linked).
193+ resolve_railway_ids
194+
103195 railway_call domain --service gateway --json > /dev/null 2>&1 || true
104196
105197 local public_domain_ref
0 commit comments