Skip to content

API Contract Guard #447

API Contract Guard

API Contract Guard #447

Workflow file for this run

# ═══════════════════════════════════════════════════════════════════════════════
# API Contract Guard — Prevent silent API shape breakage
# Issue #179 — Schema-to-UI Contract Validation (Quality Gate 9/9)
# ═══════════════════════════════════════════════════════════════════════════════
# Runs the RPC contract integration tests that validate the shape of every
# Supabase RPC function called by the frontend using Zod schemas.
# If a migration changes a column name, removes a field, or alters a return
# type, these tests FAIL before the frontend silently breaks.
#
# Contract schemas: frontend/src/lib/rpc-contracts/
# Integration tests: frontend/src/lib/rpc-contracts/__tests__/
# Unit tests: frontend/src/lib/rpc-contracts/__tests__/schema-validation.test.ts
#
# These tests require live Supabase connectivity (staging environment).
# Triggered on:
# • Every push to main (post-merge validation)
# • PRs that touch SQL migrations or contract schemas (pre-merge prevention)
# • Nightly (drift detection)
# ═══════════════════════════════════════════════════════════════════════════════
name: API Contract Guard
on:
push:
branches: [main]
paths:
- "supabase/migrations/**"
- "db/**"
- "frontend/src/lib/rpc-contract*"
- "frontend/src/lib/rpc-contracts/**"
- ".github/workflows/api-contract.yml"
pull_request:
branches: [main]
paths:
- "supabase/migrations/**"
- "db/**"
- "frontend/src/lib/rpc-contract*"
- "frontend/src/lib/rpc-contracts/**"
- ".github/workflows/api-contract.yml"
schedule:
# Nightly 03:30 UTC — detect drift from manual DB changes (staggered from quality-gate)
- cron: "30 3 * * *"
workflow_dispatch: {}
permissions:
contents: read
concurrency:
group: api-contract-${{ github.ref }}
cancel-in-progress: true
jobs:
contract-tests:
name: RPC Contract Validation
runs-on: ubuntu-latest
timeout-minutes: 10
# Skip forks — they never have staging secrets
if: github.event.pull_request.head.repo.fork != true
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# ── Fail-fast: validate required secrets before wasting CI minutes ──
- name: Validate required secrets
working-directory: .
env:
_URL: ${{ secrets.SUPABASE_URL_STAGING || secrets.NEXT_PUBLIC_SUPABASE_URL }}
_KEY: ${{ secrets.SUPABASE_ANON_KEY_STAGING || secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
run: |
missing=0
if [ -z "$_URL" ]; then
echo "::error::Neither SUPABASE_URL_STAGING nor NEXT_PUBLIC_SUPABASE_URL is set"
missing=1
fi
if [ -z "$_KEY" ]; then
echo "::error::Neither SUPABASE_ANON_KEY_STAGING nor NEXT_PUBLIC_SUPABASE_ANON_KEY is set"
missing=1
fi
if [ "$missing" -ne 0 ]; then
echo "::error::Contract tests require live Supabase connectivity — set the secrets above"
exit 1
fi
echo "All required secrets present."
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 20
cache: npm
cache-dependency-path: frontend/package-lock.json
- run: npm ci
- name: Run RPC contract tests
env:
INTEGRATION: "1"
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 }}
run: |
echo "## API Contract Test Results" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
set +e
npx vitest run rpc-contract 2>&1 | tee /tmp/contract-output.txt
exit_code=${PIPESTATUS[0]}
set -e
echo '```' >> "$GITHUB_STEP_SUMMARY"
cat /tmp/contract-output.txt >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
if [ "$exit_code" -ne 0 ]; then
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "❌ **API contract violation detected!**" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "One or more RPC functions returned unexpected shapes." >> "$GITHUB_STEP_SUMMARY"
echo "This means a migration likely changed the API contract." >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "**To reproduce locally:**" >> "$GITHUB_STEP_SUMMARY"
echo '```bash' >> "$GITHUB_STEP_SUMMARY"
echo 'cd frontend && npm run test:integration' >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "**Fix options:**" >> "$GITHUB_STEP_SUMMARY"
echo "1. Fix the migration to preserve the existing contract" >> "$GITHUB_STEP_SUMMARY"
echo "2. Update the frontend to handle the new shape" >> "$GITHUB_STEP_SUMMARY"
echo "3. Update the contract tests if the change is intentional" >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "✅ All API contracts validated successfully." >> "$GITHUB_STEP_SUMMARY"
- name: Verify health endpoint contract
env:
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 }}
run: |
# Also run the health route unit test which validates response shape
npx vitest run api/health