Skip to content

Commit 92f9cba

Browse files
mpstatonclaude
andcommitted
ci(deploy-watch): trim the token, name what the credential actually is, and refuse to call an empty read healthy
The operator confirms the token IS a project token, so "wrong kind of token" was the wrong diagnosis. Railway reports a token carrying one stray byte as flatly "not found" — identical to the message for a wrong-type token — and a secret set from a clipboard or a file is exactly where a trailing newline comes from. The token is now trimmed before use, and its raw vs trimmed length is printed so a whitespace problem is visible rather than inferred. If the probe still fails it now retries the same credential as an account token and reports whether Railway answers `me`, which distinguishes the two remaining explanations by evidence instead of by argument. Also included: an empty-service-list guard. Dropping `curl -f` to let the diagnostic print meant a failed status query would return zero rows, match zero FAILED services, and pass — a watchdog going green because it had gone blind. That is the precise failure this workflow exists to catch, so an empty read is now a hard error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QwrqK15CtSW1sWAyEzopco
1 parent 0d02999 commit 92f9cba

1 file changed

Lines changed: 36 additions & 10 deletions

File tree

.github/workflows/deploy-watch.yml

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,28 +52,43 @@ jobs:
5252
exit 1
5353
fi
5454
55+
# Trim whitespace. A secret set from a clipboard or a file almost
56+
# always carries a trailing newline, and Railway reports a token with
57+
# one stray byte as flatly "not found" — indistinguishable from the
58+
# wrong KIND of token, which is what sent the first diagnosis astray.
59+
TOKEN=$(printf '%s' "${RAILWAY_TOKEN}" | tr -d '[:space:]')
60+
61+
echo "Token shape: raw=${#RAILWAY_TOKEN} trimmed=${#TOKEN} chars"
62+
if printf '%s' "$TOKEN" | grep -qiE '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'; then
63+
echo "Token shape: matches the UUID form Railway project tokens take."
64+
else
65+
echo "Token shape: NOT a bare UUID — check for a copied prefix or a truncated paste."
66+
fi
67+
5568
api() {
56-
curl -sSf -X POST https://backboard.railway.com/graphql/v2 \
57-
-H "Project-Access-Token: ${RAILWAY_TOKEN}" \
69+
curl -sS -X POST https://backboard.railway.com/graphql/v2 \
70+
-H "Project-Access-Token: ${TOKEN}" \
5871
-H "Content-Type: application/json" \
5972
-d "$1"
6073
}
6174
6275
# The token knows which environment it belongs to, so no Railway IDs
6376
# are committed here. Swapping the secret repoints the whole workflow.
64-
# `|| true` so a non-2xx from curl -f still reaches the diagnostic
65-
# below instead of dying silently under `set -e`.
6677
PROBE=$(api '{"query":"query { projectToken { environmentId } }"}' || true)
6778
ENV_ID=$(printf '%s' "$PROBE" | jq -r '.data.projectToken.environmentId // empty')
6879
6980
if [ -z "$ENV_ID" ]; then
70-
# `projectToken` resolves ONLY for a project token. An account or
71-
# team token authenticates fine and returns null here, which is the
72-
# overwhelmingly likely cause. Print the response — it carries
73-
# Railway's own error message and contains no credential.
74-
echo "::error::Could not resolve environmentId from the token. Is it a PROJECT token?"
75-
echo "Railway response:"
81+
echo "::error::Could not resolve environmentId from the token."
82+
echo "Project-Access-Token response:"
7683
printf '%s\n' "$PROBE" | jq . 2>/dev/null || printf '%s\n' "$PROBE"
84+
85+
# Identify what the credential actually IS, so the next step is a
86+
# fact rather than a guess. An account/team token answers `me`.
87+
echo "Retrying the same token as an ACCOUNT token (Authorization: Bearer):"
88+
curl -sS -X POST https://backboard.railway.com/graphql/v2 \
89+
-H "Authorization: Bearer ${TOKEN}" \
90+
-H "Content-Type: application/json" \
91+
-d '{"query":"query { me { email } }"}' | jq . 2>/dev/null || true
7792
exit 1
7893
fi
7994
@@ -82,6 +97,17 @@ jobs:
8297
variables: {id: $id}
8398
}')" > status.json
8499
100+
# An empty service list must NEVER read as healthy. Without this, any
101+
# error on the query above yields zero rows, zero FAILED matches, and
102+
# a green run — a watchdog reporting all-clear precisely because it
103+
# could not see, which is the failure it was built to end.
104+
COUNT=$(jq -r '.data.environment.serviceInstances.edges | length // 0' status.json 2>/dev/null || echo 0)
105+
if [ "${COUNT:-0}" -eq 0 ]; then
106+
echo "::error::Railway returned no services. Refusing to report healthy on an empty read."
107+
jq . status.json 2>/dev/null || cat status.json
108+
exit 1
109+
fi
110+
85111
# FAILED/CRASHED are the states that mean "this did not ship."
86112
# Transient states (BUILDING, DEPLOYING, INITIALIZING, QUEUED) are not
87113
# failures — a run that happens to land mid-deploy must stay quiet.

0 commit comments

Comments
 (0)