Post-deploy E2E smoke #96
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Post-deploy E2E smoke | |
| # Phase 4c post-deploy E2E workflow per ROADMAP. Runs the Playwright | |
| # spec at `frontend_web/e2e/postdeploy-happy-path.spec.ts` against | |
| # the deployed staging URL. | |
| # | |
| # Trigger model: | |
| # | |
| # - Nightly cron (03:00 UTC) so regressions land in the oncall | |
| # window of the next operator review, not in the middle of a | |
| # live meeting. | |
| # - Manual `workflow_dispatch` for targeted runs after a cold-apply | |
| # or a deliberate staging change. | |
| # | |
| # Gating (two-layer): | |
| # | |
| # Layer 1 — vars.FRONTEND_DOMAIN (fork-friendly gate): | |
| # Driven by the existing `vars.FRONTEND_DOMAIN` repository variable | |
| # (provisioned in Phase 4a, documented in ADR-0027). The frontend is | |
| # served by S3 + CloudFront per ADR-0027. Forks that haven't populated | |
| # FRONTEND_DOMAIN see the job skip cleanly with operator guidance. | |
| # | |
| # Layer 2 — staging liveness (ephemeral-environment gate): | |
| # Staging is ephemeral — brought up for verification then torn to $0. | |
| # Even when FRONTEND_DOMAIN is set, the staging environment may be | |
| # down. We probe the PUBLIC gateway health endpoint directly — | |
| # `https://${GATEWAY_DOMAIN}/healthz` — instead of an AWS-internal | |
| # proxy. This is credential-free (no OIDC/IAM/KMS) and tests the | |
| # actual deployed surface. curl's outcome gives a clean three-way: | |
| # - transport failure (DNS/refused/timeout) → staging is gone → green skip | |
| # - HTTP 200 → staging up & healthy → run the smoke suite | |
| # - reachable but non-200 → staging up but broken → red | |
| # Why not the SSM probe it replaced: the /aegis/staging/cognito/* | |
| # params are written by a manual operator step (not Terraform), so | |
| # they orphan on teardown — ParameterNotFound was not a reliable | |
| # "staging is down" signal. A public liveness probe has no such proxy | |
| # gap. This mirrors the gate in nightly-cognito-integration.yml. | |
| # | |
| # Why nightly (not per-commit): post-deploy E2E tests the *deployed* | |
| # surface, not the pre-merge code. Running it per-commit would test | |
| # the current main against whatever previous version happens to be | |
| # deployed — not useful. The right cadence is "after every ArgoCD | |
| # sync, plus a nightly safety net"; the ArgoCD-sync hook is a | |
| # future slice (needs webhook plumbing on the cluster side). | |
| # | |
| # Refs: ROADMAP.md Phase 4c "Post-deploy E2E suite against staging"; | |
| # ADR-0002 Constraint 4 (webkit parity); Incident 09 rationale for | |
| # live-browser harnesses. | |
| on: | |
| schedule: | |
| # 03:00 UTC = 11:00 Taipei daylight, 10:00 Taipei standard. | |
| # Lands before the typical morning operator review. | |
| - cron: "0 3 * * *" | |
| workflow_dispatch: | |
| concurrency: | |
| group: postdeploy-e2e | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read # checkout only — the liveness gate is a public, unauthenticated curl | |
| jobs: | |
| playwright: | |
| name: Playwright against staging | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| - name: Short-circuit if FRONTEND_DOMAIN unset | |
| id: gate | |
| env: | |
| FRONTEND_DOMAIN: ${{ vars.FRONTEND_DOMAIN }} | |
| run: | | |
| if [ -z "${FRONTEND_DOMAIN:-}" ]; then | |
| echo "vars.FRONTEND_DOMAIN unset — skipping (fork-friendly default)." | |
| echo "Set repository variable FRONTEND_DOMAIN to your staging frontend" | |
| echo "host (e.g. aegis-app.staging.example.com) per" | |
| echo "See ADR-0027 for the Variables a forker must set to enable the real run." | |
| echo "should_run=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "should_run=true" >> "$GITHUB_OUTPUT" | |
| echo "url=https://${FRONTEND_DOMAIN}" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Liveness gate (credential-free): probe the PUBLIC gateway health | |
| # endpoint to decide whether staging is actually serving traffic. | |
| # No AWS OIDC/IAM/KMS — /healthz is unauthenticated. curl's outcome | |
| # classifies three ways: | |
| # - transport failure (DNS/refused/timeout, exit != 0) → staging | |
| # gone (teardown removed DNS + ALB) → green skip | |
| # - HTTP 200 → staging up & healthy → run the smoke suite | |
| # - reachable but non-200 → staging up but broken → red | |
| - name: Preflight — is staging serving? | |
| id: preflight | |
| if: steps.gate.outputs.should_run == 'true' | |
| env: | |
| GATEWAY_DOMAIN: ${{ vars.GATEWAY_DOMAIN }} | |
| run: | | |
| set -uo pipefail | |
| # GitHub's default step shell is `bash -e {0}`, so errexit is ON even | |
| # though we never set it. Turn it OFF here: a failing `HTTP=$(curl …)` | |
| # (e.g. DNS-fail when staging is torn down) would otherwise kill the | |
| # step on the first attempt — defeating the retry loop and the | |
| # transport-failure → skip classification below. We read curl's exit | |
| # code explicitly instead of letting -e act on it. | |
| set +e | |
| if [ -z "${GATEWAY_DOMAIN:-}" ]; then | |
| echo "::error title=GATEWAY_DOMAIN unset::Set repository variable GATEWAY_DOMAIN (e.g. aegis-api.staging.aws.binhsu.org) so the liveness probe can reach the gateway." | |
| exit 1 | |
| fi | |
| URL="https://${GATEWAY_DOMAIN}/healthz" | |
| # -sS: quiet but show transport errors; no -f, so we read the | |
| # status code ourselves and distinguish "down" (no connection) | |
| # from "broken" (connected, non-200). | |
| # | |
| # Retry transport failures (RC != 0) up to 3 attempts before | |
| # concluding "down": a single transient runner/DNS blip must not | |
| # produce a false-green skip while staging is actually up. Any HTTP | |
| # response (even an error code) ends the loop immediately — that's | |
| # a real answer to classify, not a transport failure. | |
| HTTP=000; RC=0 | |
| for attempt in 1 2 3; do | |
| HTTP=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 "$URL" 2>/dev/null) | |
| RC=$? | |
| [ "$RC" -eq 0 ] && break | |
| [ "$attempt" -lt 3 ] && { echo "Probe attempt ${attempt} failed (curl exit ${RC}); retrying…"; sleep 5; } | |
| done | |
| if [ "$RC" -ne 0 ]; then | |
| echo "staging_up=false" >> "$GITHUB_OUTPUT" | |
| echo "::notice title=Post-deploy E2E smoke skipped::Gateway ${URL} unreachable after 3 attempts (curl exit ${RC}) — staging is torn down. Skipping. Bring staging up to re-enable." | |
| elif [ "$HTTP" = "200" ]; then | |
| echo "staging_up=true" >> "$GITHUB_OUTPUT" | |
| echo "Gateway ${URL} returned 200 — staging is up; running E2E smoke tests." | |
| else | |
| echo "::error title=Gateway unhealthy::${URL} is reachable but returned HTTP ${HTTP} (expected 200). Staging is up but the gateway is unhealthy — failing so the problem is visible." >&2 | |
| exit 1 | |
| fi | |
| - name: Install frontend deps | |
| if: steps.preflight.outputs.staging_up == 'true' | |
| run: ./tools/scripts/frontend.sh install | |
| - name: Install Playwright browsers | |
| if: steps.preflight.outputs.staging_up == 'true' | |
| run: ./tools/scripts/frontend.sh e2e:install | |
| - name: Run post-deploy spec | |
| if: steps.preflight.outputs.staging_up == 'true' | |
| env: | |
| AEGIS_POSTDEPLOY_URL: ${{ steps.gate.outputs.url }} | |
| # Forward CI=true so the Playwright config applies its | |
| # retry=1 + github-reporter posture. | |
| CI: "true" | |
| run: | | |
| cd frontend_web | |
| # -g runs only the `postdeploy-happy-path` spec; the | |
| # existing `consent-smoke` spec stays scoped to pre-merge CI | |
| # (ci-baseline.yml) because it needs the local Vite server. | |
| node node_modules/@playwright/test/cli.js test \ | |
| --reporter=github \ | |
| -g "post-deploy happy-path smoke" | |
| - name: Upload Playwright report on failure | |
| if: failure() && steps.preflight.outputs.staging_up == 'true' | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: playwright-report-postdeploy | |
| path: frontend_web/playwright-report/ | |
| retention-days: 7 |