Skip to content

Post-deploy E2E smoke #96

Post-deploy E2E smoke

Post-deploy E2E smoke #96

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