familynido/.github/workflows/e2e.yml
dependabot[bot] 5fa4588cf8
Bump actions/setup-node from 4 to 6 (#4)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-13 00:56:03 +02:00

300 lines
11 KiB
YAML

name: E2E (Playwright)
on:
pull_request:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: e2e-${{ github.ref }}
cancel-in-progress: true
# Run JS-based actions (checkout, setup-dotnet, setup-node, etc.) on Node 24
# instead of the deprecated Node 20 runtime. Node 20 is being removed from
# GitHub-hosted runners on 2026-09-16; this opt-in silences the warning and
# avoids the forced switch on 2026-06-02.
# Ref: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
jobs:
playwright:
runs-on: ubuntu-latest
timeout-minutes: 25
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: familynido
POSTGRES_USER: familynido
POSTGRES_PASSWORD: familynido
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U familynido -d familynido"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
# The seeded testers' credentials. Stable across runs because the
# E2ETestDataSeeder upserts on email — it never rotates an existing
# password — and these values are only ever real on this ephemeral
# Postgres instance, never in production.
E2E_USER_EMAIL: e2e-a@familynido.test
E2E_USER_PASSWORD: SuperSecret123!
E2E_USER_B_EMAIL: e2e-b@familynido.test
E2E_USER_B_PASSWORD: SuperSecret123!
E2E_USER_B_NAME: Tester B
E2E_BASE_URL: http://localhost:5173
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up .NET 10
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Set up Node 22
uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: web/familynido-web/package-lock.json
- name: Restore .NET
run: dotnet restore
- name: Build & publish API
run: |
dotnet publish src/FamilyNido.Api -c Release -o publish/api --no-restore
ls -la publish/api/FamilyNido.Api.dll
- name: Install SPA deps
working-directory: web/familynido-web
run: npm ci
- name: Build SPA (es + en bundles)
working-directory: web/familynido-web
run: npm run build
- name: Install nginx
run: sudo apt-get update -y && sudo apt-get install -y nginx
- name: Configure nginx (proxy /api → :8080, serve SPA at :5173)
run: |
set -euxo pipefail
BUILT="$GITHUB_WORKSPACE/web/familynido-web/dist/familynido-web/browser"
if [ ! -d "$BUILT/es" ] || [ ! -d "$BUILT/en" ]; then
echo "Localized SPA bundles not found at $BUILT" >&2
ls -la "$BUILT" >&2 || true
exit 1
fi
# Copy the SPA out of /home/runner/... — nginx runs as www-data
# and can't traverse into the runner's home tree by default. /var/www
# is the canonical "nginx-readable" location.
SPA_ROOT=/var/www/familynido-e2e
sudo rm -rf "$SPA_ROOT"
sudo mkdir -p "$SPA_ROOT"
sudo cp -r "$BUILT"/. "$SPA_ROOT/"
sudo chown -R www-data:www-data "$SPA_ROOT"
sudo find "$SPA_ROOT" -type d -exec chmod 755 {} \;
sudo find "$SPA_ROOT" -type f -exec chmod 644 {} \;
echo "── /var/www/familynido-e2e tree ──"
ls -la "$SPA_ROOT"
ls -la "$SPA_ROOT/es" | head -n 5
sudo tee /etc/nginx/sites-available/familynido-e2e > /dev/null <<EOF
server {
listen 5173 default_server;
server_name _;
root $SPA_ROOT;
index index.html;
absolute_redirect off;
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 60s;
}
location = / {
if (\$http_accept_language ~* "^en") { return 302 /en/; }
return 302 /es/;
}
location /es/ { try_files \$uri /es/index.html; }
location /en/ { try_files \$uri /en/index.html; }
# Anything else (e.g. /tasks, /calendar) is a Playwright spec
# navigating without the locale prefix. Redirect through to
# /es/<path> so the SPA loads with the right base href instead
# of nginx returning its default 404.
location / {
return 302 /es\$request_uri;
}
}
EOF
sudo rm -f /etc/nginx/sites-enabled/default
sudo ln -sf /etc/nginx/sites-available/familynido-e2e /etc/nginx/sites-enabled/familynido-e2e
sudo nginx -t
- name: Start API + wait for ready (combined step)
env:
ASPNETCORE_ENVIRONMENT: Testing
# Bind to all interfaces so curl on 127.0.0.1 reaches Kestrel
# regardless of how the runner resolves localhost.
ASPNETCORE_URLS: http://+:8080
# Disable Serilog buffering — write to console with line-buffered
# output so we see startup messages in real time.
Serilog__MinimumLevel: Information
DOTNET_PRINT_TELEMETRY_MESSAGE: 'false'
DOTNET_NOLOGO: '1'
ConnectionStrings__Postgres: Host=localhost;Port=5432;Database=familynido;Username=familynido;Password=familynido
Family__AutoMigrate: 'true'
Seed__E2E__Enabled: 'true'
# Pin the seeded family's timezone to UTC so the date the browser
# computes via `new Date()` (UTC in CI) and the date the server
# resolves via `family.TimeZone` always agree. Otherwise a CI run
# crossing midnight UTC pushes a task into a day the server's
# "today" doesn't include yet, and `/api/household-tasks/today`
# returns an empty list.
Seed__E2E__TimeZone: 'UTC'
Seed__E2E__UserAEmail: ${{ env.E2E_USER_EMAIL }}
Seed__E2E__UserAPassword: ${{ env.E2E_USER_PASSWORD }}
Seed__E2E__UserBEmail: ${{ env.E2E_USER_B_EMAIL }}
Seed__E2E__UserBPassword: ${{ env.E2E_USER_B_PASSWORD }}
Seed__E2E__UserBDisplayName: ${{ env.E2E_USER_B_NAME }}
Email__Enabled: 'false'
Email__AppBaseUrl: http://localhost:5173
# Calendar sync is dormant in CI — the loop kicks off but never
# acts because no Google account is linked.
Calendar__SyncIntervalMinutes: '999999'
run: |
set -uxo pipefail
mkdir -p logs
echo "── dotnet --info ──"
dotnet --info | head -n 30
echo "── DLL stat ──"
ls -la "$GITHUB_WORKSPACE/publish/api/FamilyNido.Api.dll"
# `stdbuf -o0 -e0` forces unbuffered stdio so any framework or
# Serilog writes appear in api.log immediately, not after a 4 KB
# block fills. cd into the publish dir so config files next to
# the DLL (appsettings.json, etc.) are found.
cd "$GITHUB_WORKSPACE/publish/api"
stdbuf -o0 -e0 dotnet ./FamilyNido.Api.dll \
> "$GITHUB_WORKSPACE/logs/api.log" 2>&1 &
API_PID=$!
echo "$API_PID" > "$GITHUB_WORKSPACE/logs/api.pid"
cd "$GITHUB_WORKSPACE"
echo "API started with PID $API_PID"
# 5 s grace period for first writes / JIT warm-up.
sleep 5
echo "── after 5 s, api.log ──"
cat logs/api.log || true
echo "── ports listening ──"
ss -tlnp 2>/dev/null | head -n 30 || true
# Wait loop with periodic snapshots.
for i in $(seq 1 90); do
if curl -fsS http://127.0.0.1:8080/health/ready > /dev/null; then
echo "API ready after ${i}s"
exit 0
fi
if ! kill -0 "$API_PID" 2>/dev/null; then
echo "API process exited; dumping log:" >&2
cat logs/api.log >&2 || true
exit 1
fi
if [ $((i % 10)) -eq 0 ]; then
echo "── snapshot @ ${i}s ──" >&2
ss -tlnp 2>/dev/null | grep -E ':(8080|5432) ' >&2 || true
echo "── api.log tail ──" >&2
tail -n 40 logs/api.log >&2 || true
fi
sleep 1
done
echo "API never became ready" >&2
echo "── Final full api.log ──" >&2
cat logs/api.log >&2 || true
exit 1
- name: Start nginx
run: sudo systemctl restart nginx
- name: Wait for SPA root
run: |
set -uxo pipefail
for i in $(seq 1 30); do
if curl -fsS -o /dev/null -L http://127.0.0.1:5173/es/; then
echo "SPA ready after ${i}s"
exit 0
fi
sleep 1
done
echo "SPA never became reachable. Diagnostics:" >&2
echo "── curl -i (verbose response) ──" >&2
curl -i http://127.0.0.1:5173/es/ 2>&1 | head -n 30 >&2 || true
echo "── nginx error.log ──" >&2
sudo tail -n 50 /var/log/nginx/error.log >&2 || true
echo "── nginx access.log ──" >&2
sudo tail -n 20 /var/log/nginx/access.log >&2 || true
echo "── nginx process ──" >&2
ps aux | grep nginx | grep -v grep >&2 || true
exit 1
- name: Install Playwright browsers
working-directory: web/familynido-web
run: npx playwright install --with-deps chromium
- name: Run Playwright
working-directory: web/familynido-web
run: npx playwright test
- name: Stop API
if: always()
run: |
if [ -f logs/api.pid ]; then
kill "$(cat logs/api.pid)" || true
fi
- name: Upload Playwright report on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: web/familynido-web/playwright-report
retention-days: 14
- name: Upload Playwright traces on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-traces
path: web/familynido-web/test-results
retention-days: 14
- name: Upload API log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: api-log
path: logs/api.log
retention-days: 14