Skip to content

Guard notification cache-refresh hook against partial service worker mocks #340

Guard notification cache-refresh hook against partial service worker mocks

Guard notification cache-refresh hook against partial service worker mocks #340

Workflow file for this run

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