-
Notifications
You must be signed in to change notification settings - Fork 2
328 lines (319 loc) Β· 11.8 KB
/
Copy pathfrontend-quality.yml
File metadata and controls
328 lines (319 loc) Β· 11.8 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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
name: Frontend quality gates
on:
pull_request:
branches: [main, revamped-ui]
paths:
- 'web/**'
- 'scripts/screenshots.js'
- 'cmd/routetemplategen/**'
- 'internal/api/webvitals/routetemplates_gen.go'
- '.github/workflows/frontend-quality.yml'
push:
branches: [main, revamped-ui]
paths:
- 'web/**'
- 'scripts/screenshots.js'
- 'cmd/routetemplategen/**'
- 'internal/api/webvitals/routetemplates_gen.go'
- '.github/workflows/frontend-quality.yml'
workflow_dispatch:
inputs:
production_base_url:
description: 'Optional deployed URL for authenticated smoke (no mocks)'
required: false
type: string
concurrency:
group: frontend-quality-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
defaults:
run:
working-directory: web
jobs:
contract:
name: Route registry and test discovery
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
cache-dependency-path: web/package-lock.json
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache-dependency-path: go.sum
- run: npm ci --legacy-peer-deps
# ββ Route-template freshness chain ββββββββββββββββββββββββββββββββββββ
# App.tsx -> routeRegistry.ts -> internal/api/webvitals (generated Go).
# routeRegistry.ts is the source of truth for privacy-safe route
# templating on BOTH sides. A `:param` route added to App.tsx but never
# regenerated means its opaque value (share token, customer slug) is
# never templated and lands verbatim in a Prometheus label and in
# buffered browser-error payloads. Both links are checked here so the
# gate covers main AND revamped-ui.
- name: Route registry is fresh (App.tsx -> routeRegistry.ts)
run: node scripts/generate-route-registry.mjs --check
- name: Backend route table is fresh (routeRegistry.ts -> Go artifact)
working-directory: .
run: go run ./cmd/routetemplategen --check
- run: npm run e2e:routes
- run: npm run e2e:contract
- run: npm run e2e:scenario-contract
- run: npm run e2e:list
chromium-quality:
name: Chromium responsive, a11y, keyboard, and performance
runs-on: ubuntu-latest
needs: contract
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
cache-dependency-path: web/package-lock.json
- run: npm ci --legacy-peer-deps
- run: npx playwright install --with-deps chromium
- name: Build once and run Chromium gates
shell: bash
env:
E2E_MOCKS: '1'
run: |
set -euo pipefail
npm run e2e:build
mkdir -p test-results
node node_modules/vite/bin/vite.js preview \
--outDir e2e/.app-dist --host 127.0.0.1 --port 4173 --strictPort \
> test-results/preview.log 2>&1 &
PREVIEW_PID=$!
cleanup() {
kill "$PREVIEW_PID" 2>/dev/null || true
wait "$PREVIEW_PID" 2>/dev/null || true
}
trap cleanup EXIT
for attempt in $(seq 1 120); do
if curl --fail --silent --show-error http://127.0.0.1:4173/ >/dev/null; then
break
fi
if ! kill -0 "$PREVIEW_PID" 2>/dev/null; then
cat test-results/preview.log
exit 1
fi
if [ "$attempt" -eq 120 ]; then
cat test-results/preview.log
exit 1
fi
sleep 1
done
export E2E_BASE_URL=http://127.0.0.1:4173
export E2E_SKIP_WEBSERVER=1
npm run e2e:quality:run
npm run e2e:performance:run
npm run e2e:a11y:run
if grep -Eiq 'ECONNREFUSED|proxy error|http proxy error' test-results/preview.log; then
cat test-results/preview.log
exit 1
fi
- name: Upload Chromium failure evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: chromium-quality-report
if-no-files-found: ignore
retention-days: 14
path: |
web/test-results/
web/playwright-report/
cross-browser:
name: ${{ matrix.browser }} critical smoke
runs-on: ubuntu-latest
needs: contract
strategy:
fail-fast: false
matrix:
browser: [firefox, webkit]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
cache-dependency-path: web/package-lock.json
- run: npm ci --legacy-peer-deps
- run: npx playwright install --with-deps ${{ matrix.browser }}
- name: Build once and run lightweight engine smoke
shell: bash
env:
E2E_MOCKS: '1'
run: |
set -euo pipefail
npm run e2e:build
mkdir -p test-results
node node_modules/vite/bin/vite.js preview \
--outDir e2e/.app-dist --host 127.0.0.1 --port 4173 --strictPort \
> test-results/preview.log 2>&1 &
PREVIEW_PID=$!
cleanup() {
kill "$PREVIEW_PID" 2>/dev/null || true
wait "$PREVIEW_PID" 2>/dev/null || true
}
trap cleanup EXIT
for attempt in $(seq 1 120); do
if curl --fail --silent --show-error http://127.0.0.1:4173/ >/dev/null; then
break
fi
if ! kill -0 "$PREVIEW_PID" 2>/dev/null; then
cat test-results/preview.log
exit 1
fi
if [ "$attempt" -eq 120 ]; then
cat test-results/preview.log
exit 1
fi
sleep 1
done
export E2E_BASE_URL=http://127.0.0.1:4173
export E2E_SKIP_WEBSERVER=1
npx playwright test --project=${{ matrix.browser }}-smoke
if grep -Eiq 'ECONNREFUSED|proxy error|http proxy error' test-results/preview.log; then
cat test-results/preview.log
exit 1
fi
- name: Upload engine failure evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.browser }}-smoke-report
if-no-files-found: ignore
retention-days: 14
path: |
web/test-results/
web/playwright-report/
visual:
name: Deliberate visual snapshot gate (Windows baseline)
runs-on: windows-latest
needs: contract
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
cache-dependency-path: web/package-lock.json
- run: npm ci --legacy-peer-deps
- run: npx playwright install chromium
- name: Build once and compare committed snapshots
env:
E2E_MOCKS: '1'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
npm run e2e:build
if ($LASTEXITCODE -ne 0) { throw "E2E build failed: $LASTEXITCODE" }
New-Item -ItemType Directory -Force test-results | Out-Null
$preview = Start-Process -FilePath node -ArgumentList @(
'node_modules/vite/bin/vite.js',
'preview',
'--outDir', 'e2e/.app-dist',
'--host', '127.0.0.1',
'--port', '4173',
'--strictPort'
) -PassThru -RedirectStandardOutput test-results/preview.log -RedirectStandardError test-results/preview-error.log
try {
$ready = $false
for ($attempt = 1; $attempt -le 120; $attempt++) {
if ($preview.HasExited) {
Get-Content test-results/preview*.log -ErrorAction SilentlyContinue
throw "Vite preview exited before readiness: $($preview.ExitCode)"
}
try {
$response = Invoke-WebRequest -UseBasicParsing http://127.0.0.1:4173/ -TimeoutSec 2
if ($response.StatusCode -eq 200) { $ready = $true; break }
} catch {}
Start-Sleep -Seconds 1
}
if (-not $ready) {
Get-Content test-results/preview*.log -ErrorAction SilentlyContinue
throw 'Vite preview was not ready within 120 seconds'
}
$env:E2E_BASE_URL = 'http://127.0.0.1:4173'
$env:E2E_SKIP_WEBSERVER = '1'
npm run e2e:visual:run
if ($LASTEXITCODE -ne 0) { throw "Visual suite failed: $LASTEXITCODE" }
$proxyLeak = Select-String -Path test-results/preview*.log -Pattern 'ECONNREFUSED|proxy error|http proxy error' -ErrorAction SilentlyContinue
if ($proxyLeak) {
$proxyLeak | ForEach-Object { Write-Error $_.Line }
throw 'E2E preview emitted a proxy/network escape error'
}
} finally {
if (-not $preview.HasExited) {
Stop-Process -Id $preview.Id
$preview.WaitForExit(5000)
}
}
- name: Upload visual diffs
if: always()
uses: actions/upload-artifact@v4
with:
name: visual-regression-report
if-no-files-found: ignore
retention-days: 14
path: |
web/test-results/
web/playwright-report/
authenticated-production-smoke:
name: Optional authenticated production smoke
if: ${{ github.event_name == 'workflow_dispatch' && inputs.production_base_url != '' }}
runs-on: ubuntu-latest
needs: contract
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
cache-dependency-path: web/package-lock.json
- run: npm ci --legacy-peer-deps
- run: npx playwright install --with-deps chromium
- name: Materialize ephemeral storage state
env:
STORAGE_STATE_B64: ${{ secrets.E2E_STORAGE_STATE_B64 }}
run: |
if [ -n "$STORAGE_STATE_B64" ]; then
echo "$STORAGE_STATE_B64" | base64 --decode > e2e/.auth-state.json
echo "E2E_STORAGE_STATE=e2e/.auth-state.json" >> "$GITHUB_ENV"
else
echo "No E2E_STORAGE_STATE_B64 secret supplied; running an unauthenticated deployed smoke."
fi
- name: Run deployed smoke without mocks
id: production_smoke
env:
E2E_BASE_URL: ${{ inputs.production_base_url }}
E2E_MOCKS: '0'
E2E_SENSITIVE: '1'
run: |
set +e
npx playwright test --project=chromium-smoke cross-browser.smoke.spec.ts production-journeys.smoke.spec.ts
EXIT_CODE=$?
echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT"
STATUS=$([ "$EXIT_CODE" -eq 0 ] && echo passed || echo failed)
printf '{"status":"%s","project":"chromium-smoke","recording":"disabled"}\n' "$STATUS" > authenticated-smoke-status.json
exit 0
- name: Remove ephemeral credentials
if: always()
run: |
rm -f e2e/.auth-state.json
rm -rf test-results playwright-report blob-report
- name: Upload sanitized deployed smoke status
if: always()
uses: actions/upload-artifact@v4
with:
name: authenticated-production-smoke-status
if-no-files-found: error
retention-days: 7
path: web/authenticated-smoke-status.json
- name: Enforce deployed smoke result
if: always() && steps.production_smoke.outputs.exit_code != '0'
run: exit 1