-
Notifications
You must be signed in to change notification settings - Fork 0
173 lines (162 loc) · 8.09 KB
/
Copy pathpostdeploy-e2e.yml
File metadata and controls
173 lines (162 loc) · 8.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
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