Guard notification cache-refresh hook against partial service worker mocks #340
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| qa-backend: | |
| name: Backend Tests & Coverage | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Validate repository hygiene | |
| run: | | |
| python3 - <<'PY' | |
| from pathlib import Path | |
| root = Path(".") | |
| duplicate_memory = root / "docs" / "memories.json" | |
| if duplicate_memory.exists(): | |
| raise SystemExit("docs/memories.json is not allowed under docs/.") | |
| patterns = [ | |
| "qa-*.png", | |
| "qa-*.md", | |
| "qa-*.txt", | |
| "*-desktop-*.png", | |
| "*-desktop-*.md", | |
| "*-desktop-*.txt", | |
| "*-mobile-*.png", | |
| "desktop-*.png", | |
| "mobile-*.png", | |
| "dashboard-*.png", | |
| "dashboard-*.txt", | |
| "monthly-review-*.png", | |
| "reset-passkey-*.png", | |
| "visualizer-*.png", | |
| "visualizer-*.md", | |
| "visualizer-*.txt", | |
| "wallboard-*.png", | |
| "wallboard-*.md", | |
| "wallboard-*.txt", | |
| "weekly-review-*.png", | |
| "weekly-review-*.md", | |
| "weekly-review-*.txt", | |
| "landing-*.md", | |
| "landing-*.png", | |
| "landing-*.txt", | |
| ] | |
| offenders = sorted({path.name for pattern in patterns for path in root.glob(pattern)}) | |
| if offenders: | |
| raise SystemExit( | |
| "Root-level QA artifacts must live under docs/qa-artifacts/: " | |
| + ", ".join(offenders) | |
| ) | |
| print("Repository hygiene checks passed.") | |
| PY | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: "8.0.x" | |
| - name: Restore dependencies | |
| working-directory: ./src/backend | |
| run: dotnet restore | |
| - name: Run backend format/lint checks | |
| working-directory: ./src/backend | |
| run: dotnet format LifeOS.sln --verify-no-changes --verbosity minimal --no-restore | |
| - name: Build | |
| working-directory: ./src/backend | |
| run: dotnet build --no-restore --configuration Release | |
| - name: Run tests with coverage | |
| working-directory: ./src/backend | |
| run: | | |
| dotnet test --no-build --configuration Release \ | |
| --filter "FullyQualifiedName!~PostgresProof" \ | |
| --collect:"XPlat Code Coverage" \ | |
| --settings coverlet.runsettings \ | |
| --results-directory ./TestResults \ | |
| --logger "trx;LogFileName=test-results.trx" \ | |
| -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura | |
| - name: Evaluate backend coverage floor | |
| if: always() | |
| run: | | |
| python3 - <<'PY' | |
| import glob | |
| import xml.etree.ElementTree as ET | |
| threshold = 0.55 | |
| files = glob.glob("src/backend/TestResults/**/coverage.cobertura.xml", recursive=True) | |
| if not files: | |
| raise SystemExit("No backend coverage file found.") | |
| root = ET.parse(files[0]).getroot() | |
| rate = float(root.attrib.get("line-rate", "0")) | |
| pct = round(rate * 100, 2) | |
| print(f"Backend line coverage: {pct}%") | |
| if rate < threshold: | |
| print(f"::error::Backend coverage {pct}% is below required floor {threshold*100:.0f}%") | |
| raise SystemExit(1) | |
| PY | |
| - name: Upload test results | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: backend-test-results | |
| path: ./src/backend/TestResults/ | |
| - name: Upload coverage report | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: backend-coverage-report | |
| path: ./src/backend/TestResults/**/coverage.cobertura.xml | |
| - name: Enforce backend coverage floor | |
| working-directory: ./src/backend | |
| run: | | |
| python3 - <<'PY' | |
| import glob | |
| import os | |
| import xml.etree.ElementTree as ET | |
| threshold = 0.55 | |
| reports = glob.glob("TestResults/**/coverage.cobertura.xml", recursive=True) | |
| if not reports: | |
| raise SystemExit("No backend coverage report found.") | |
| latest = max(reports, key=os.path.getmtime) | |
| root = ET.parse(latest).getroot() | |
| line_rate = float(root.attrib.get("line-rate", "0")) | |
| print(f"Backend line coverage: {line_rate:.2%} (threshold {threshold:.0%})") | |
| if line_rate < threshold: | |
| raise SystemExit("Backend coverage below required threshold.") | |
| PY | |
| - name: Generate coverage report summary | |
| if: always() | |
| working-directory: ./src/backend | |
| run: | | |
| echo "## Backend Test Coverage Report" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ -f "TestResults/test-results.trx" ]; then | |
| echo "✅ Tests completed successfully" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| qa-backend-postgres-proof: | |
| name: Backend Postgres Proof | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| services: | |
| postgres: | |
| image: postgres:17 | |
| env: | |
| POSTGRES_USER: lifeos | |
| POSTGRES_PASSWORD: change_me_database_password | |
| POSTGRES_DB: postgres | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U lifeos -d postgres" | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 20 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: "8.0.x" | |
| - name: Restore dependencies | |
| working-directory: ./src/backend | |
| run: dotnet restore | |
| - name: Build backend test project | |
| working-directory: ./src/backend | |
| run: dotnet build LifeOS.Tests/LifeOS.Tests.csproj --no-restore --configuration Release | |
| - name: Run Postgres proof integration lane | |
| working-directory: ./src/backend | |
| env: | |
| POSTGRES_PROOF_ADMIN_CONNECTION_STRING: Host=127.0.0.1;Port=5432;Database=postgres;Username=lifeos;Password=change_me_database_password | |
| run: dotnet test LifeOS.Tests/LifeOS.Tests.csproj --no-build --configuration Release --filter FullyQualifiedName~PostgresProof | |
| qa-frontend: | |
| name: Frontend Tests & Coverage | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: "npm" | |
| cache-dependency-path: ./src/frontend/package-lock.json | |
| - name: Install dependencies | |
| working-directory: ./src/frontend | |
| run: npm ci | |
| - name: Run linting | |
| working-directory: ./src/frontend | |
| run: npm run lint | |
| - name: Build frontend | |
| working-directory: ./src/frontend | |
| run: npm run build | |
| - name: Run tests with coverage | |
| working-directory: ./src/frontend | |
| run: npm run test:coverage | |
| - name: Enforce frontend coverage floor | |
| working-directory: ./src/frontend | |
| run: | | |
| node - <<'NODE' | |
| const fs = require('fs'); | |
| const path = 'coverage/coverage-summary.json'; | |
| if (!fs.existsSync(path)) { | |
| throw new Error('No frontend coverage summary found.'); | |
| } | |
| const summary = JSON.parse(fs.readFileSync(path, 'utf8')).total; | |
| const threshold = 60; | |
| const metrics = ['lines', 'statements', 'functions', 'branches']; | |
| for (const metric of metrics) { | |
| const pct = summary?.[metric]?.pct ?? 0; | |
| console.log(`Frontend ${metric} coverage: ${pct}% (threshold ${threshold}%)`); | |
| if (pct < threshold) { | |
| throw new Error(`Frontend ${metric} coverage below required threshold.`); | |
| } | |
| } | |
| NODE | |
| - name: Setup .NET for Playwright backend | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: "8.0.x" | |
| - name: Restore backend dependencies for Playwright | |
| working-directory: ./src/backend | |
| run: dotnet restore LifeOS.Api/LifeOS.Api.csproj | |
| - name: Warm backend build for Playwright | |
| working-directory: ./src/backend | |
| run: dotnet build LifeOS.Api/LifeOS.Api.csproj --no-restore | |
| - name: Install Playwright browser | |
| working-directory: ./src/frontend | |
| run: npx playwright install --with-deps chromium | |
| - name: Run Playwright E2E tests | |
| working-directory: ./src/frontend | |
| run: npm run test:e2e | |
| - name: Upload coverage report | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: frontend-coverage-report | |
| path: ./src/frontend/coverage/ | |
| - name: Generate coverage report summary | |
| if: always() | |
| working-directory: ./src/frontend | |
| run: | | |
| echo "## Frontend Test Coverage Report" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ -d "coverage" ]; then | |
| echo "✅ Coverage report generated" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| qa-compose: | |
| name: Compose Readiness | |
| runs-on: ubuntu-latest | |
| needs: | |
| - qa-backend | |
| - qa-backend-postgres-proof | |
| - qa-frontend | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Run compose readiness smoke | |
| run: make docker-ready | |
| - name: Tear down compose stack | |
| if: always() | |
| run: docker compose down -v --remove-orphans | |
| publish-backend-amd64: | |
| name: Publish Backend (amd64) | |
| runs-on: ubuntu-latest | |
| needs: | |
| - qa-compose | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Login to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Build and push backend image (amd64) | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: ./src/backend/Dockerfile | |
| push: true | |
| platforms: linux/amd64 | |
| tags: ${{ secrets.DOCKERHUB_USERNAME }}/lifeos-backend:latest-amd64 | |
| publish-backend-arm64: | |
| name: Publish Backend (arm64) | |
| runs-on: ubuntu-24.04-arm | |
| needs: | |
| - qa-compose | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Login to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Build and push backend image (arm64) | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: ./src/backend/Dockerfile | |
| push: true | |
| platforms: linux/arm64 | |
| tags: ${{ secrets.DOCKERHUB_USERNAME }}/lifeos-backend:latest-arm64 | |
| publish-backend-manifest: | |
| name: Publish Backend Manifest | |
| runs-on: ubuntu-latest | |
| needs: | |
| - publish-backend-amd64 | |
| - publish-backend-arm64 | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| steps: | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Login to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Create and push manifest | |
| run: | | |
| docker buildx imagetools create -t ${{ secrets.DOCKERHUB_USERNAME }}/lifeos-backend:latest \ | |
| ${{ secrets.DOCKERHUB_USERNAME }}/lifeos-backend:latest-amd64 \ | |
| ${{ secrets.DOCKERHUB_USERNAME }}/lifeos-backend:latest-arm64 | |
| publish-frontend-amd64: | |
| name: Publish Frontend (amd64) | |
| runs-on: ubuntu-latest | |
| needs: | |
| - qa-compose | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Login to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Build and push frontend image (amd64) | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: ./src/frontend | |
| file: ./src/frontend/Dockerfile | |
| build-contexts: | | |
| repo_root=. | |
| push: true | |
| platforms: linux/amd64 | |
| tags: ${{ secrets.DOCKERHUB_USERNAME }}/lifeos-frontend:latest-amd64 | |
| build-args: | | |
| BUILD_SHA=${{ github.sha }} | |
| publish-frontend-arm64: | |
| name: Publish Frontend (arm64) | |
| runs-on: ubuntu-24.04-arm | |
| needs: | |
| - qa-compose | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Login to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Build and push frontend image (arm64) | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: ./src/frontend | |
| file: ./src/frontend/Dockerfile | |
| build-contexts: | | |
| repo_root=. | |
| push: true | |
| platforms: linux/arm64 | |
| tags: ${{ secrets.DOCKERHUB_USERNAME }}/lifeos-frontend:latest-arm64 | |
| build-args: | | |
| BUILD_SHA=${{ github.sha }} | |
| publish-frontend-manifest: | |
| name: Publish Frontend Manifest | |
| runs-on: ubuntu-latest | |
| needs: | |
| - publish-frontend-amd64 | |
| - publish-frontend-arm64 | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| steps: | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Login to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Create and push manifest | |
| run: | | |
| docker buildx imagetools create -t ${{ secrets.DOCKERHUB_USERNAME }}/lifeos-frontend:latest \ | |
| ${{ secrets.DOCKERHUB_USERNAME }}/lifeos-frontend:latest-amd64 \ | |
| ${{ secrets.DOCKERHUB_USERNAME }}/lifeos-frontend:latest-arm64 |