-
Notifications
You must be signed in to change notification settings - Fork 0
fix: harden Railway operations and staging checks #918
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,7 +26,7 @@ const root = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); | |
| // Vars that MUST be supplied as deployment/CI secrets (never committed). Each is | ||
| // asserted to exist in the canonical name set below, so this list cannot silently | ||
| // drift from the schema. | ||
| const EXPECTED_SECRETS = [ | ||
| export const EXPECTED_GITHUB_SECRETS = [ | ||
| "SUPABASE_SERVICE_ROLE_KEY", | ||
| "OPENAI_API_KEY", | ||
| "RAG_QUERY_HASH_SECRET", | ||
|
|
@@ -35,6 +35,13 @@ const EXPECTED_SECRETS = [ | |
| "E2E_USER_PASSWORD", | ||
| ]; | ||
|
|
||
| export const EXPECTED_RAILWAY_SECRETS = [ | ||
| "SUPABASE_SERVICE_ROLE_KEY", | ||
| "OPENAI_API_KEY", | ||
| "RAG_QUERY_HASH_SECRET", | ||
| "HEALTH_DEEP_PROBE_SECRET", | ||
| ]; | ||
|
|
||
| /** Zod schema keys from env.ts: lines shaped like ` NAME: z.…`. */ | ||
| export function parseEnvSchemaNames(envTsText) { | ||
| return [...envTsText.matchAll(/^\s*([A-Z][A-Z0-9_]*)\s*:\s*z\./gm)].map((m) => m[1]); | ||
|
|
@@ -58,16 +65,23 @@ export function computeParity({ canonical, liveNames, expectedSecrets }) { | |
| }; | ||
| } | ||
|
|
||
| /** Extract Railway variable names from the CLI's JSON object without exposing values. */ | ||
| export function parseRailwayVariableNames(raw) { | ||
| const parsed = JSON.parse(raw); | ||
| if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") { | ||
| throw new Error("Railway variable JSON must be an object"); | ||
| } | ||
| return Object.keys(parsed); | ||
| } | ||
|
|
||
| function ghSecretNames() { | ||
| const raw = execFileSync("gh", ["secret", "list", "--json", "name"], { encoding: "utf8" }); | ||
| return JSON.parse(raw).map((s) => s.name); | ||
| } | ||
|
|
||
| function railwayVarNames() { | ||
| // `railway variables` prints a table; --json/-k vary by CLI version, so parse | ||
| // UPPER_SNAKE tokens defensively. Names only. | ||
| const raw = execFileSync("railway", ["variables"], { encoding: "utf8" }); | ||
| return [...new Set([...raw.matchAll(/\b([A-Z][A-Z0-9_]{2,})\b/g)].map((m) => m[1]))]; | ||
| const raw = execFileSync("railway", ["variable", "list", "--json"], { encoding: "utf8" }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| return parseRailwayVariableNames(raw); | ||
| } | ||
|
|
||
| function main() { | ||
|
|
@@ -81,19 +95,21 @@ function main() { | |
| const problems = []; | ||
|
|
||
| // Self-consistency: every expected secret must be a name the app/CI actually knows. | ||
| const unknownExpected = EXPECTED_SECRETS.filter((name) => !canonical.has(name)); | ||
| const expectedSecrets = new Set([...EXPECTED_GITHUB_SECRETS, ...EXPECTED_RAILWAY_SECRETS]); | ||
| const unknownExpected = [...expectedSecrets].filter((name) => !canonical.has(name)); | ||
| if (unknownExpected.length > 0) { | ||
| problems.push( | ||
| `Expected-secret names not found in env.ts/check-ci-env (typo or drift): ${unknownExpected.join(", ")}`, | ||
| ); | ||
| } | ||
|
|
||
| console.log(`Known env names: ${canonical.size} (env.ts schema + check-ci-env).`); | ||
| console.log(`Expected secrets: ${EXPECTED_SECRETS.join(", ")}`); | ||
| console.log(`Expected GitHub secrets: ${EXPECTED_GITHUB_SECRETS.join(", ")}`); | ||
| console.log(`Expected Railway secrets: ${EXPECTED_RAILWAY_SECRETS.join(", ")}`); | ||
|
|
||
| for (const [flag, enabled, label, getter] of [ | ||
| ["--gh", useGh, "GitHub secrets", ghSecretNames], | ||
| ["--railway", useRailway, "Railway variables", railwayVarNames], | ||
| for (const [flag, enabled, label, getter, sourceExpectedSecrets] of [ | ||
| ["--gh", useGh, "GitHub secrets", ghSecretNames, EXPECTED_GITHUB_SECRETS], | ||
| ["--railway", useRailway, "Railway variables", railwayVarNames, EXPECTED_RAILWAY_SECRETS], | ||
| ]) { | ||
| if (!enabled) { | ||
| console.log(`(${label}: skipped — pass ${flag} to check; names only, no values)`); | ||
|
|
@@ -109,7 +125,7 @@ function main() { | |
| const { missingSecrets, unknownLive } = computeParity({ | ||
| canonical: [...canonical], | ||
| liveNames, | ||
| expectedSecrets: EXPECTED_SECRETS, | ||
| expectedSecrets: sourceExpectedSecrets, | ||
| }); | ||
| console.log(`\n${label}: ${liveNames.length} names.`); | ||
| if (missingSecrets.length > 0) problems.push(`${label}: missing expected secret(s): ${missingSecrets.join(", ")}`); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new offline/no-key staging profile is still contradicted by
docs/launch-operator-runbook.md:121-125, which tells operators to setOPENAI_API_KEYandRAG_PROVIDER_MODE=auto. When staging is recreated from that launch runbook, it will re-enable provider-backed paths and invalidate the deterministic/no-spend staging checks this change adds; update that runbook or point it at this offline profile.Useful? React with 👍 / 👎.