fix(ci): fail production deploys loudly when the environment is unconfigured (#1143) - #1151
Conversation
…figured (#1143) The `production` GitHub environment holds **zero** secrets while `deploy-production` references 24 of them, and GitHub resolves a missing environment secret to an empty string with no warning. Today that surfaces five steps in as an SSH auth failure — a symptom that says nothing about the cause. Populating only the SSH secrets would be worse: the job would proceed and write a remote .env containing `AUTH_SECRET=`, an empty JWT signing key. Recorded decision (option 3 of the three the issue lists): **guard, do not delete.** Staging is the only deploy target today — the operator's words: "no users, we're getting to beta on staging" — but the repo has open Launch issues, so removing the job would only have to be rewritten. Populating `production` stays an operator decision this cannot make; failing loudly first is not. A preflight step runs FIRST, before checkout and SSH, and refuses to continue unless all 15 genuinely-required secrets are present and non-blank (a secret set to " " counts as missing). It names every one that is absent and says how to set it. Values come from env, never interpolated into the script — the #1130 rule. Also drops `environment.url: https://codeframe.example.com`. A placeholder domain renders on the GitHub deployments page as a live link to a target that has never existed. The bare `KNOWN_GAPS` exemption in tests/test_workflow_lint_wiring_1122.py was a promise nobody checked. It is now `GUARDED` and conditional: the exemption asserts the preflight step still exists, so deleting the guard re-fails the scanner. Three new tests cover it — the step must be first (behind SSH setup it reports nothing the SSH failure had not), every secret it wires into its own env must appear in the list it iterates (a guard that omits one says "configured" while a blank AUTH_SECRET reaches the remote .env), and no fictional URL. Verified all three bite: removing AUTH_SECRET from the checked list, and renaming the step, each fail the suite.
|
Claude finished @frankbria's task in 4m 26s —— View job GLM precision review completeScope: concrete defects only (logic errors, security, data loss, contract breaks) in this PR's workflow + test changes.
Result: ✅ No defects found. Summary posted as a separate comment on the PR. The preflight shell logic is correct under |
ReviewSolid, well-scoped fix. I read the full What's good:
Minor, non-blocking observations:
No bugs, security issues, or test-coverage gaps that should block this. The exit-1 preflight logic was also validated standalone per the PR description, and the injection-safety pattern (values via |
|
✅ GLM precision review: no defects found. Scoped to concrete defects only (logic errors, security, data loss, contract breaks). What was checked in
|
|
Checks: all 14 green. Full backend suite 6481 passed, 49 skipped (507s), Third-party review (
Demo of the guard's own logic, run standalone against set / blank / empty values — the whitespace case is the one a naive $ A="ok"; B=" "; C="" # then the preflight's loop verbatim
missing: B CThe production job itself cannot be run end to end — that is the condition this guard exists to report, and it is now what the job says instead of an SSH auth error. |
Closes #1143.
The decision, recorded
The issue asks for one of three: populate
production, delete the job, or guard it. Option 3 — guard, do not delete.Staging is the only deploy target today, and the operator's framing is explicit: "Nothing's live, there are no users. We're just trying to get to 'beta stage' on a staging server." But the repo carries open Launch: issues, so deleting
deploy-productionwould only have to be rewritten. Populatingproductionis an operator decision a code change cannot make. Failing loudly first is not.Why the current state is worse than "it doesn't work"
productionholds zero secrets whiledeploy-productionreferences 24. GitHub resolves a missing environment secret to an empty string with no warning..envcontainingAUTH_SECRET=. That is the JWT signing key.What this adds
A preflight step that runs first — before checkout, before SSH — and refuses to continue unless all 15 genuinely-required secrets are present and non-blank (a secret set to
" "counts as missing):Values are read from env, never interpolated into the run script — the rule #1130 established after finding that an expression inside a
run:block executes on the runner.Also drops
environment.url: https://codeframe.example.com. A placeholder domain renders on the GitHub deployments page as a live link to a target that has never existed — the same class of thing as the stale staging URL fixed in #1145.The exemption became conditional
tests/test_workflow_lint_wiring_1122.pycarriedKNOWN_GAPS = {("deploy.yml", "deploy-production")}— a bare exemption, which is a promise nobody checks. It is nowGUARDED, and the exemption itself asserts the preflight step still exists. Delete the guard and the scanner re-fails.Three new tests:
AUTH_SECRETreaches the remote.env— worse than no guardVerified all three bite: removing
AUTH_SECRETfrom the checked list fails the coverage test; renaming the step fails three tests including the original scanner.Acceptance criteria
AUTH_SECRETconfirmed non-empty before any deploy writes the remote.env— twice now: the preflight, plus the job's existing fail-fast at the.envstepTestEnvironmentSecretsAreReachableis goneproductionholds every secret and one deploy is verified — deliberately not done. That is the operator action this PR cannot take, and it is exactly what the preflight now demands before a deploy can proceedTesting
The production job cannot be exercised end to end for the same reason it needs this guard. What is verified:
actionlint(with shellcheck) clean, the preflight's shell logic run standalone against set/blank/empty values, the three scanner tests proven to fail when the guard is removed, and the full backend suite (reported below).