Skip to content

Nightly Suite

Nightly Suite #97

Workflow file for this run

# ═══════════════════════════════════════════════════════════════════════════════
# Nightly Suite — Comprehensive test coverage + data integrity
# CI Architecture Redesign — absorbs data-audit.yml into nightly schedule
# ═══════════════════════════════════════════════════════════════════════════════
# Runs all Playwright projects (including visual regression + authenticated),
# full unit test coverage, and data integrity audit against live Supabase.
#
# Target runtime: < 15 minutes (jobs run in parallel)
# ═══════════════════════════════════════════════════════════════════════════════
name: Nightly Suite
on:
schedule:
- cron: "0 2 * * *" # 2:00 UTC daily
workflow_dispatch: {}
permissions:
contents: read
concurrency:
group: nightly
cancel-in-progress: true # Only latest nightly matters
env:
BASELINE_COMPREHENSIVE: 900
BASELINE_DATA_AUDIT: 300
jobs:
# ─────────────────────────────────────────────────────────────────────────
# Job 1: Full frontend test suite (unit + all Playwright projects)
# ─────────────────────────────────────────────────────────────────────────
comprehensive:
name: Full Test Suite
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: 30
defaults:
run:
working-directory: frontend
steps:
- name: Record job start
id: timer
run: echo "start=$(date +%s)" >> "$GITHUB_OUTPUT"
working-directory: .
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20
cache: npm
cache-dependency-path: frontend/package-lock.json
- run: npm ci
- name: Cache Next.js build
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: frontend/.next/cache
key: next-cache-${{ runner.os }}-${{ hashFiles('frontend/package-lock.json') }}-${{ hashFiles('frontend/src/**') }}
restore-keys: |
next-cache-${{ runner.os }}-${{ hashFiles('frontend/package-lock.json') }}-
next-cache-${{ runner.os }}-
- name: Build
run: npx next build
timeout-minutes: 5
- name: Unit tests with coverage
run: npx vitest run --coverage
timeout-minutes: 5
- name: Get Playwright version
id: pw-version
run: echo "version=$(node -e "console.log(require('@playwright/test/package.json').version)")" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
id: pw-cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/ms-playwright
key: pw-${{ runner.os }}-${{ steps.pw-version.outputs.version }}
- name: Install Playwright browsers
if: steps.pw-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium
timeout-minutes: 8
- name: Install Playwright system deps
if: steps.pw-cache.outputs.cache-hit == 'true'
run: npx playwright install-deps chromium
timeout-minutes: 8
- name: Seed QA fixture data
run: |
OUTPUT=$(node tests/quality/seed-fixtures.mjs 2>&1 1>/tmp/qa-ids.txt || true)
echo "$OUTPUT"
if [ -f /tmp/qa-ids.txt ] && [ -s /tmp/qa-ids.txt ]; then
cat /tmp/qa-ids.txt >> "$GITHUB_ENV"
else
echo "::warning::QA fixture seeding skipped or failed — tests may use fallback IDs"
fi
env:
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SUPABASE_URL_STAGING || secrets.NEXT_PUBLIC_SUPABASE_URL }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY_STAGING || secrets.SUPABASE_SERVICE_ROLE_KEY }}
- name: Full Playwright (all projects including visual)
run: npx playwright test 2>&1 | tee ../playwright-stdout.log
timeout-minutes: 20
env:
# Visual regression disabled until baseline screenshots are generated
# and committed from a Linux CI run. Re-enable after running:
# npx playwright test --project=visual-smoke --update-snapshots
# VISUAL_REGRESSION: "true"
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SUPABASE_URL_STAGING || secrets.NEXT_PUBLIC_SUPABASE_URL }}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY_STAGING || secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY_STAGING || secrets.SUPABASE_SERVICE_ROLE_KEY }}
- name: Upload Playwright report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ !cancelled() }}
with:
name: nightly-playwright-report
path: |
frontend/playwright-report/
frontend/test-results/
playwright-stdout.log
retention-days: 30
- name: Alert on comprehensive failure
if: failure()
env:
ALERT_WEBHOOK_URL: ${{ secrets.ALERT_WEBHOOK_URL }}
run: |
if [ -n "$ALERT_WEBHOOK_URL" ]; then
curl -sf -X POST "$ALERT_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d '{"text": "Nightly comprehensive tests FAILED \u2014 https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"}'
fi
working-directory: .
- name: CI runtime summary
if: ${{ !cancelled() }}
working-directory: .
run: |
end=$(date +%s); start=${{ steps.timer.outputs.start }}; d=$((end-start))
echo "## Nightly Comprehensive: $((d/60))m $((d%60))s" >> "$GITHUB_STEP_SUMMARY"
pct=0; [ "$BASELINE_COMPREHENSIVE" -gt 0 ] && pct=$(( (d-BASELINE_COMPREHENSIVE)*100/BASELINE_COMPREHENSIVE ))
[ "$pct" -gt 30 ] && echo "::warning::Nightly comprehensive regression: ${d}s (+${pct}% vs ${BASELINE_COMPREHENSIVE}s)"
mkdir -p ci-timings
echo '{"job":"comprehensive","duration_seconds":'$d',"baseline_seconds":'$BASELINE_COMPREHENSIVE',"delta_pct":'$pct'}' > ci-timings/comprehensive.json
- name: Upload CI timings
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ci-timings-comprehensive
path: ci-timings/
retention-days: 30
# ─────────────────────────────────────────────────────────────────────────
# Job 2: Data Integrity Audit (runs in parallel with comprehensive)
# Absorbed from the former data-audit.yml
# ─────────────────────────────────────────────────────────────────────────
data-audit:
name: Data Integrity Audit
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: 15
steps:
- name: Record job start
id: timer
run: echo "start=$(date +%s)" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
cache: "pip"
- run: pip install -r requirements.txt
- name: Run Data Integrity Audit
env:
SUPABASE_URL: ${{ secrets.SUPABASE_URL || secrets.SUPABASE_URL_STAGING || secrets.NEXT_PUBLIC_SUPABASE_URL }}
SUPABASE_SERVICE_KEY: ${{ secrets.SUPABASE_SERVICE_KEY || secrets.SUPABASE_SERVICE_ROLE_KEY_STAGING || secrets.SUPABASE_SERVICE_ROLE_KEY }}
run: |
if [ -z "$SUPABASE_URL" ] || [ -z "$SUPABASE_SERVICE_KEY" ]; then
echo "::warning::Data integrity audit skipped — SUPABASE_URL or SUPABASE_SERVICE_KEY secret not configured"
exit 0
fi
python run_data_audit.py
- name: Upload Audit Report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: data-audit-report
path: audit-reports/
retention-days: 90
- name: Post Summary
if: always()
run: |
REPORT=$(cat audit-reports/audit_*.json 2>/dev/null || echo '{}')
CRITICAL=$(echo "$REPORT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('summary',{}).get('critical',0))" 2>/dev/null || echo "?")
WARNINGS=$(echo "$REPORT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('summary',{}).get('warnings',0))" 2>/dev/null || echo "?")
INFO=$(echo "$REPORT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('summary',{}).get('info',0))" 2>/dev/null || echo "?")
TOTAL=$(echo "$REPORT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('summary',{}).get('total_findings',0))" 2>/dev/null || echo "?")
echo "## Data Integrity Audit" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Severity | Count |" >> $GITHUB_STEP_SUMMARY
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| 🔴 Critical | $CRITICAL |" >> $GITHUB_STEP_SUMMARY
echo "| 🟠 Warning | $WARNINGS |" >> $GITHUB_STEP_SUMMARY
echo "| 🔵 Info | $INFO |" >> $GITHUB_STEP_SUMMARY
echo "| **Total** | **$TOTAL** |" >> $GITHUB_STEP_SUMMARY
- name: Alert on Critical Findings
if: failure()
env:
ALERT_WEBHOOK_URL: ${{ secrets.ALERT_WEBHOOK_URL }}
run: |
if [ -n "$ALERT_WEBHOOK_URL" ]; then
curl -sf -X POST "$ALERT_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d '{"text": "Nightly audit: CRITICAL issues — https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"}'
fi
- name: CI runtime summary
if: ${{ !cancelled() }}
run: |
end=$(date +%s); start=${{ steps.timer.outputs.start }}; d=$((end-start))
echo "## Data Audit: $((d/60))m $((d%60))s" >> "$GITHUB_STEP_SUMMARY"
pct=0; [ "$BASELINE_DATA_AUDIT" -gt 0 ] && pct=$(( (d-BASELINE_DATA_AUDIT)*100/BASELINE_DATA_AUDIT ))
[ "$pct" -gt 30 ] && echo "::warning::Data audit regression: ${d}s (+${pct}% vs ${BASELINE_DATA_AUDIT}s)"
mkdir -p ci-timings
echo '{"job":"data-audit","duration_seconds":'$d',"baseline_seconds":'$BASELINE_DATA_AUDIT',"delta_pct":'$pct'}' > ci-timings/data-audit.json
- name: Upload CI timings
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ci-timings-data-audit
path: ci-timings/
retention-days: 30