Skip to content

feat: zero-touch library mode with pending review queue #466

feat: zero-touch library mode with pending review queue

feat: zero-touch library mode with pending review queue #466

Workflow file for this run

name: CI
on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:
jobs:
migration-lint:
name: Migration Lint
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Set up Python
run: uv python install 3.12
- name: Lint migrations
working-directory: backend
run: uv run python scripts/lint_migrations.py
backend-lint:
name: Backend Lint
runs-on: self-hosted
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies
run: uv sync --extra dev
- name: Lint with ruff
run: uv run ruff check .
- name: Type check with mypy
run: uv run mypy app --ignore-missing-imports
- name: Lint profile contracts
run: uv run python scripts/lint_profile_contracts.py
backend-test-contract:
name: Backend Test (Contract)
runs-on: [self-hosted, Linux]
timeout-minutes: 4
defaults:
run:
working-directory: backend
services:
postgres:
image: pgvector/pgvector:pg16
env:
POSTGRES_USER: familiar
POSTGRES_PASSWORD: familiar
POSTGRES_DB: familiar
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies
run: uv sync --extra dev
- name: Apply migrations
env:
DATABASE_URL: postgresql+asyncpg://familiar:familiar@localhost:5432/familiar
run: uv run python -m alembic upgrade head
- name: Migration preflight
env:
DATABASE_URL: postgresql+asyncpg://familiar:familiar@localhost:5432/familiar
run: uv run python -c "from app.db.migration_preflight import assert_database_at_head; assert_database_at_head()"
- name: Run tests
env:
DATABASE_URL: postgresql+asyncpg://familiar:familiar@localhost:5432/familiar
REDIS_URL: redis://localhost:6379/0
# Baseline 2026-03-18: TBD (set from first CI run). Threshold set conservatively low.
run: uv run pytest tests/test_contract_error_shapes.py tests/test_contract_auth_matrix.py tests/test_contract_envelope_parity.py tests/test_contract_happy_path.py tests/test_migrations.py -v --tb=short --cov=app --cov-report=term-missing --cov-fail-under=12 --junit-xml=results-contract.xml
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-contract
path: backend/results-contract.xml
- name: Check skip budget
if: always()
working-directory: backend
run: |
SKIP_COUNT=$(grep -c '<skipped' results-contract.xml 2>/dev/null || echo 0)
echo "Skipped tests: $SKIP_COUNT (budget: 2)"
if [ "$SKIP_COUNT" -gt 2 ]; then
echo "::error::Skip budget exceeded: $SKIP_COUNT skipped (budget: 2)"
exit 1
fi
backend-test-core:
name: Backend Test (Core)
runs-on: [self-hosted, Linux]
timeout-minutes: 10
defaults:
run:
working-directory: backend
services:
postgres:
image: pgvector/pgvector:pg16
env:
POSTGRES_USER: familiar
POSTGRES_PASSWORD: familiar
POSTGRES_DB: familiar
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies
run: uv sync --extra dev
- name: Apply migrations
env:
DATABASE_URL: postgresql+asyncpg://familiar:familiar@localhost:5432/familiar
run: uv run python -m alembic upgrade head
- name: Migration preflight
env:
DATABASE_URL: postgresql+asyncpg://familiar:familiar@localhost:5432/familiar
run: uv run python -c "from app.db.migration_preflight import assert_database_at_head; assert_database_at_head()"
- name: Run tests
env:
DATABASE_URL: postgresql+asyncpg://familiar:familiar@localhost:5432/familiar
REDIS_URL: redis://localhost:6379/0
run: uv run pytest --ignore=tests/test_contract_error_shapes.py --ignore=tests/test_contract_auth_matrix.py --ignore=tests/test_contract_envelope_parity.py --ignore=tests/test_migrations.py -k "not integration" -v --tb=short --cov=app --cov-report=term-missing --cov-fail-under=35 --junit-xml=results-core.xml
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-core
path: backend/results-core.xml
- name: Check skip budget
if: always()
working-directory: backend
run: |
SKIP_COUNT=$(grep -c '<skipped' results-core.xml 2>/dev/null || echo 0)
echo "Skipped tests: $SKIP_COUNT (budget: 5)"
if [ "$SKIP_COUNT" -gt 5 ]; then
echo "::error::Skip budget exceeded: $SKIP_COUNT skipped (budget: 5)"
exit 1
fi
backend-test-integration:
name: Backend Test (Integration)
runs-on: [self-hosted, Linux]
timeout-minutes: 15
defaults:
run:
working-directory: backend
services:
postgres:
image: pgvector/pgvector:pg16
env:
POSTGRES_USER: familiar
POSTGRES_PASSWORD: familiar
POSTGRES_DB: familiar
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies
run: uv sync --extra dev
- name: Apply migrations
env:
DATABASE_URL: postgresql+asyncpg://familiar:familiar@localhost:5432/familiar
run: uv run python -m alembic upgrade head
- name: Migration preflight
env:
DATABASE_URL: postgresql+asyncpg://familiar:familiar@localhost:5432/familiar
run: uv run python -c "from app.db.migration_preflight import assert_database_at_head; assert_database_at_head()"
- name: Run tests
env:
DATABASE_URL: postgresql+asyncpg://familiar:familiar@localhost:5432/familiar
REDIS_URL: redis://localhost:6379/0
# Baseline 2026-03-18: TBD (set from first CI run). Threshold set conservatively low; more variance due to --reruns.
run: uv run pytest -k "integration" -v --tb=short --reruns 2 --reruns-delay 1 --cov=app --cov-report=term-missing --cov-fail-under=5 --junit-xml=results-integration.xml
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-integration
path: backend/results-integration.xml
- name: Check skip budget
if: always()
working-directory: backend
run: |
SKIP_COUNT=$(grep -c '<skipped' results-integration.xml 2>/dev/null || echo 0)
echo "Skipped tests: $SKIP_COUNT (budget: 3)"
if [ "$SKIP_COUNT" -gt 3 ]; then
echo "::error::Skip budget exceeded: $SKIP_COUNT skipped (budget: 3)"
exit 1
fi
- name: Check for flaky tests (reruns)
if: always()
working-directory: backend
run: |
RERUN_COUNT=$(grep -c 'rerun' results-integration.xml 2>/dev/null || echo 0)
if [ "$RERUN_COUNT" -gt 0 ]; then
echo "::warning::$RERUN_COUNT test(s) required reruns - these may be flaky"
grep 'rerun' results-integration.xml || true
fi
ios-native-tests:
name: iOS Native AppTests
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Run iOS unit tests
working-directory: packages/ios/native
run: |
xcodebuild test \
-scheme App \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
-resultBundlePath TestResults \
CODE_SIGNING_ALLOWED=NO \
| xcpretty || true
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: ios-test-results
path: packages/ios/native/TestResults
retention-days: 7
frontend-lint:
name: Frontend Lint
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: latest
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm --filter @familiar/frontend run lint
- name: Audio Guardrail Checks
run: pnpm --filter @familiar/frontend run check:audio-guardrails
- name: Dependency Boundary Checks
run: pnpm --filter @familiar/frontend run check:boundaries
frontend-test:
name: Frontend Test
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: latest
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run unit tests
run: pnpm --filter @familiar/frontend test
frontend-build:
name: Frontend Build
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: latest
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm --filter @familiar/web run build
- name: Bundle Budget Checks
env:
# Temporary bootstrap hard ceiling while Phase 6 split work reduces entry size.
# Targets are still enforced as warnings by the checker.
BUNDLE_ENTRY_GZIP_HARD_KB: '460'
run: pnpm --filter @familiar/web run check:bundle-budgets
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: frontend-dist
path: packages/web/dist
retention-days: 7
- name: Upload performance artifacts
uses: actions/upload-artifact@v4
with:
name: frontend-perf-metrics
path: artifacts/perf
retention-days: 7
docker-build:
name: Docker Build
runs-on: [self-hosted, Linux]
timeout-minutes: 30
needs: [backend-lint, backend-test-contract, backend-test-core, backend-test-integration, frontend-lint, frontend-test]
steps:
- uses: actions/checkout@v4
- name: Free up disk space
run: |
docker system prune -af --volumes || true
df -h || true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
uses: docker/build-push-action@v6
timeout-minutes: 25
with:
context: .
file: docker/Dockerfile
push: false
tags: familiar:test
e2e-test:
name: E2E Tests (Playwright)
runs-on: [self-hosted, Linux]
needs: [docker-build]
services:
postgres:
image: pgvector/pgvector:pg16
env:
POSTGRES_USER: familiar
POSTGRES_PASSWORD: familiar
POSTGRES_DB: familiar
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Set up Python
run: uv python install 3.12
- name: Install backend dependencies
working-directory: backend
run: uv sync --extra dev
- name: Apply migrations
working-directory: backend
env:
DATABASE_URL: postgresql+asyncpg://familiar:familiar@localhost:5432/familiar
run: uv run python -m alembic upgrade head
- name: Migration preflight
working-directory: backend
env:
DATABASE_URL: postgresql+asyncpg://familiar:familiar@localhost:5432/familiar
run: uv run python -c "from app.db.migration_preflight import assert_database_at_head; assert_database_at_head()"
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: latest
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Build frontend for E2E tests
run: |
pnpm --filter @familiar/web run build
mkdir -p backend/static
cp -r packages/web/dist/* backend/static/
- name: Start backend server
working-directory: backend
env:
DATABASE_URL: postgresql+asyncpg://familiar:familiar@localhost:5432/familiar
REDIS_URL: redis://localhost:6379/0
MUSIC_LIBRARY_PATH: ${{ github.workspace }}/backend/tests/fixtures/audio
run: |
uv run uvicorn app.main:app --host 0.0.0.0 --port 4401 &
sleep 10
- name: Install Playwright browsers
working-directory: packages/web
run: npx playwright install --with-deps chromium
- name: Run Playwright tests
working-directory: packages/web
env:
BASE_URL: http://localhost:4401
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
SPOTIFY_CLIENT_ID: ${{ secrets.SPOTIFY_CLIENT_ID }}
SPOTIFY_CLIENT_SECRET: ${{ secrets.SPOTIFY_CLIENT_SECRET }}
LASTFM_API_KEY: ${{ secrets.LASTFM_API_KEY }}
LASTFM_API_SECRET: ${{ secrets.LASTFM_API_SECRET }}
# Exclude screenshot specs - they're for README docs, not CI testing
run: npx playwright test --grep-invert="screenshot"
- name: Check for flaky E2E tests (retry-only passes)
if: always()
working-directory: packages/web
run: |
if [ ! -f playwright-report/results.json ]; then
echo "No results file found, skipping flake check"
exit 0
fi
node -e "
const r = require('./playwright-report/results.json');
const flaky = (r.suites || []).flatMap(s => (s.suites || []).flatMap(ss => ss.specs || []).concat(s.specs || []))
.filter(s => s.tests?.some(t => t.results?.length > 1 && t.results?.at(-1)?.status === 'passed'))
|| [];
if (flaky.length > 15) {
console.log('::warning::' + flaky.length + ' test(s) passed only after retry (flaky):');
flaky.forEach(s => console.log(' - ' + s.title));
process.exit(1);
}
console.log('No flaky tests detected');
"
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: packages/web/playwright-report/
retention-days: 7