#!/usr/bin/env bash # Phase 4.1 §23 — proves the CI pipeline (infra/ci/gitea-workflows/build-and-test.yml) fails # CLOSED on every listed failure mode, i.e. the underlying command genuinely exits nonzero on # broken input, which is what makes a Gitea/GitHub Actions step (and therefore the job, and # therefore `needs: regression` gating build-images/deploy-staging) fail. Doesn't need a real # CI run — checks the actual commands the workflow calls, directly, the same way Actions would # invoke them. set -uo pipefail # NOT -e — this script's whole point is deliberately triggering failures and checking their exit codes cd "$(dirname "${BASH_SOURCE[0]}")/../../.." # repo root FAIL=0 expect_nonzero() { local desc="$1"; shift "$@" >/tmp/ci-fail-drill-out 2>&1 local code=$? if [ "$code" -eq 0 ]; then echo " FAIL — $desc (command succeeded, but it should have failed)"; cat /tmp/ci-fail-drill-out; FAIL=1 else echo " ok — $desc correctly failed (exit $code)" fi } echo "Phase 4.1 CI-fails-closed drill" echo echo "== 1. Test/syntax-check failure ==" BAD_JS="$(mktemp).js" echo 'function broken( {' > "$BAD_JS" expect_nonzero "node --check on syntactically broken JS" node --check "$BAD_JS" rm -f "$BAD_JS" echo "== 2. Frontend build failure ==" cp frontend/src/App.jsx /tmp/App.jsx.bak echo 'this is not valid jsx <<<' >> frontend/src/App.jsx (cd frontend && expect_nonzero "npm run build with a broken component" npm run build) cp /tmp/App.jsx.bak frontend/src/App.jsx; rm -f /tmp/App.jsx.bak echo "== 3. Image build failure ==" TMPDOCKERFILE="$(mktemp -d)/Dockerfile" cat > "$TMPDOCKERFILE" <<'EOF' FROM node:20-slim COPY this-file-does-not-exist.js /app/ EOF expect_nonzero "docker build with a COPY of a nonexistent file" docker build -q -f "$TMPDOCKERFILE" "$(dirname "$TMPDOCKERFILE")" echo "== 4. Config validation failure ==" BAD_COMPOSE="$(mktemp -d)/docker-compose.yml" cat > "$BAD_COMPOSE" <<'EOF' services: a: image: alpine a: image: alpine EOF expect_nonzero "docker compose config on a file with a duplicate service key" docker compose -f "$BAD_COMPOSE" config echo "== 5. Migration preflight failure ==" ( cd platform/api && expect_nonzero "runMigrations() against an unreachable Postgres" \ env PLATFORM_DB_URL="postgres://nobody:nothing@127.0.0.1:1/nope" \ node -e "require('./src/db').runMigrations().then(()=>process.exit(0)).catch((e)=>{console.error(e.message);process.exit(1)})" ) echo "== 6. Staging smoke test failure ==" expect_nonzero "staging-smoke-test.sh with no STAGING_GATEWAY_URL set" \ env -u STAGING_GATEWAY_URL bash infra/scripts/staging-smoke-test.sh expect_nonzero "staging-smoke-test.sh against an unreachable staging URL" \ env STAGING_GATEWAY_URL="http://127.0.0.1:1" bash infra/scripts/staging-smoke-test.sh echo if [ "$FAIL" = "1" ]; then echo "DRILL FAILED — at least one failure mode did NOT fail closed. See above." exit 1 fi echo "OK: every listed failure mode genuinely exits nonzero — CI fails closed on all of them." echo "NOT TESTED HERE: an actual Gitea Actions run (needs Gitea Actions enabled on the real" echo "instance and this workflow file activated at .gitea/workflows/ — see build-and-test.yml's" echo "own header comment). This proves the underlying commands, which is what actually" echo "determines pass/fail once a real run happens."