Security: harden Content-Security-Policy and verify report pipeline #3
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: Tests | ||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| branches: [main] | ||
| workflow_dispatch: | ||
| inputs: | ||
| force_full_run: | ||
| description: 'Force a full test run' | ||
| type: boolean | ||
| default: false | ||
| jobs: | ||
| changes: | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| backend: ${{ steps.filter.outputs.backend }} | ||
| client: ${{ steps.filter.outputs.client }} | ||
| force_full_run: ${{ github.event.inputs.force_full_run || 'false' }} | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| - uses: dorny/paths-filter@v4 | ||
| id: filter | ||
| with: | ||
| filters: | | ||
| backend: | ||
| - 'backend/**' | ||
| - 'shared/**' | ||
| client: | ||
| - 'client/**' | ||
| - 'shared/**' | ||
| test-client: | ||
| name: Test Client | ||
| runs-on: ubuntu-latest | ||
| needs: changes | ||
| if: ${{ needs.changes.outputs.force_full_run == 'true' || needs.changes.outputs.client == 'true' }} | ||
| defaults: | ||
| run: | ||
| working-directory: client | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| with: | ||
| fetch-depth: 0 # Fetch all history for coverage comparison | ||
| - uses: actions/setup-node@v7 | ||
| with: | ||
| node-version: "20" | ||
| cache: "npm" | ||
| cache-dependency-path: client/package-lock.json | ||
| - name: Install dependencies | ||
| run: npm ci | ||
| - name: Run client unit tests with coverage | ||
| run: npm test -- --coverage --run | ||
| continue-on-error: false | ||
| - name: Check coverage thresholds | ||
| id: coverage-check | ||
| run: | | ||
| echo "Checking coverage thresholds..." | ||
| npm test -- --coverage --run --reporter=json > coverage-output.json || true | ||
| # Extract coverage summary | ||
| if [ -f coverage/coverage-summary.json ]; then | ||
| echo "Coverage report found" | ||
| cat coverage/coverage-summary.json | ||
| # Parse thresholds (lines: 80%, branches: 75%, functions: 85%, statements: 80%) | ||
| LINES=$(jq '.total.lines.pct' coverage/coverage-summary.json) | ||
| BRANCHES=$(jq '.total.branches.pct' coverage/coverage-summary.json) | ||
| FUNCTIONS=$(jq '.total.functions.pct' coverage/coverage-summary.json) | ||
| STATEMENTS=$(jq '.total.statements.pct' coverage/coverage-summary.json) | ||
| echo "lines_coverage=$LINES" >> $GITHUB_OUTPUT | ||
| echo "branches_coverage=$BRANCHES" >> $GITHUB_OUTPUT | ||
| echo "functions_coverage=$FUNCTIONS" >> $GITHUB_OUTPUT | ||
| echo "statements_coverage=$STATEMENTS" >> $GITHUB_OUTPUT | ||
| FAILED=0 | ||
| if (( $(echo "$LINES < 80" | bc -l) )); then | ||
| echo "❌ Line coverage ($LINES%) is below threshold (80%)" | ||
| FAILED=1 | ||
| fi | ||
| if (( $(echo "$BRANCHES < 75" | bc -l) )); then | ||
| echo "❌ Branch coverage ($BRANCHES%) is below threshold (75%)" | ||
| FAILED=1 | ||
| fi | ||
| if (( $(echo "$FUNCTIONS < 85" | bc -l) )); then | ||
| echo "❌ Function coverage ($FUNCTIONS%) is below threshold (85%)" | ||
| FAILED=1 | ||
| fi | ||
| if (( $(echo "$STATEMENTS < 80" | bc -l) )); then | ||
| echo "❌ Statement coverage ($STATEMENTS%) is below threshold (80%)" | ||
| FAILED=1 | ||
| fi | ||
| if [ $FAILED -eq 1 ]; then | ||
| echo "threshold_passed=false" >> $GITHUB_OUTPUT | ||
| exit 1 | ||
| else | ||
| echo "✅ All coverage thresholds passed" | ||
| echo "threshold_passed=true" >> $GITHUB_OUTPUT | ||
| fi | ||
| else | ||
| echo "⚠️ Coverage summary not found" | ||
| echo "threshold_passed=unknown" >> $GITHUB_OUTPUT | ||
| exit 1 | ||
| fi | ||
| - name: Upload coverage reports to Codecov | ||
| uses: codecov/codecov-action@v4 | ||
| with: | ||
| files: ./client/coverage/lcov.info | ||
| flags: client | ||
| name: client-coverage | ||
| fail_ci_if_error: false | ||
| token: ${{ secrets.CODECOV_TOKEN }} | ||
| - name: Comment PR with coverage | ||
| if: github.event_name == 'pull_request' | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| // Read coverage summary | ||
| let coverageSummary = ''; | ||
| try { | ||
| const summaryPath = path.join(process.cwd(), 'client/coverage/coverage-summary.json'); | ||
| const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8')); | ||
| const total = summary.total; | ||
| const lines = total.lines.pct.toFixed(2); | ||
| const branches = total.branches.pct.toFixed(2); | ||
| const functions = total.functions.pct.toFixed(2); | ||
| const statements = total.statements.pct.toFixed(2); | ||
| // Determine status emojis | ||
| const lineStatus = lines >= 80 ? '✅' : '❌'; | ||
| const branchStatus = branches >= 75 ? '✅' : '❌'; | ||
| const functionStatus = functions >= 85 ? '✅' : '❌'; | ||
| const statementStatus = statements >= 80 ? '✅' : '❌'; | ||
| coverageSummary = `## 📊 Client Test Coverage Report | ||
| | Metric | Coverage | Threshold | Status | | ||
| |--------|----------|-----------|--------| | ||
| | Lines | ${lines}% | 80% | ${lineStatus} | | ||
| | Branches | ${branches}% | 75% | ${branchStatus} | | ||
| | Functions | ${functions}% | 85% | ${functionStatus} | | ||
| | Statements | ${statements}% | 80% | ${statementStatus} | | ||
| ${lines >= 80 && branches >= 75 && functions >= 85 && statements >= 80 ? '✅ All coverage thresholds passed!' : '⚠️ Some coverage thresholds not met. Please add more tests.'} | ||
| [View detailed coverage report](https://codecov.io/gh/${{ github.repository }}/pull/${{ github.event.pull_request.number }}) | ||
| `; | ||
| } catch (error) { | ||
| coverageSummary = '⚠️ Unable to read coverage summary'; | ||
| } | ||
| // Find existing comment | ||
| const { data: comments } = await github.rest.issues.listComments({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.issue.number, | ||
| }); | ||
| const botComment = comments.find(comment => | ||
| comment.user.type === 'Bot' && | ||
| comment.body.includes('📊 Client Test Coverage Report') | ||
| ); | ||
| if (botComment) { | ||
| // Update existing comment | ||
| await github.rest.issues.updateComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| comment_id: botComment.id, | ||
| body: coverageSummary | ||
| }); | ||
| } else { | ||
| // Create new comment | ||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.issue.number, | ||
| body: coverageSummary | ||
| }); | ||
| } | ||
| - uses: actions/upload-artifact@v7 | ||
| if: always() | ||
| with: | ||
| name: client-coverage-report | ||
| path: client/coverage/ | ||
| test-backend: | ||
| name: Test Backend | ||
| runs-on: ubuntu-latest | ||
| needs: changes | ||
| if: ${{ needs.changes.outputs.force_full_run == 'true' || needs.changes.outputs.backend == 'true' }} | ||
| defaults: | ||
| run: | ||
| working-directory: backend | ||
| env: | ||
| NODE_ENV: test | ||
| SUPABASE_URL: ${{ secrets.SUPABASE_URL }} | ||
| SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} | ||
| JWT_SECRET: test-secret | ||
| ADMIN_API_KEY: test-admin-key | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| - uses: actions/setup-node@v7 | ||
| with: | ||
| node-version: "20" | ||
| cache: "npm" | ||
| cache-dependency-path: backend/package-lock.json | ||
| - name: Install dependencies | ||
| run: npm install | ||
| - name: Run tests with coverage | ||
| run: npm test -- --coverage --ci | ||
| - uses: actions/upload-artifact@v7 | ||
| if: always() | ||
| with: | ||
| name: coverage-report | ||
| path: backend/coverage/ | ||
| test-sdk: | ||
| name: Test SDK | ||
| runs-on: ubuntu-latest | ||
| needs: changes | ||
| if: ${{ needs.changes.outputs.force_full_run == 'true' }} | ||
| defaults: | ||
| run: | ||
| working-directory: sdk | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| - uses: actions/setup-node@v7 | ||
| with: | ||
| node-version: "20" | ||
| cache: "npm" | ||
| cache-dependency-path: sdk/package-lock.json | ||
| - name: Install dependencies | ||
| run: npm install | ||
| - name: Run tests with coverage (thresholds enforced) | ||
| run: npx jest --coverage --ci | ||
| - uses: actions/upload-artifact@v7 | ||
| if: always() | ||
| with: | ||
| name: sdk-coverage-report | ||
| path: sdk/coverage/ | ||
| test-shared: | ||
| name: Test Shared | ||
| runs-on: ubuntu-latest | ||
| needs: changes | ||
| if: ${{ needs.changes.outputs.force_full_run == 'true' }} | ||
| defaults: | ||
| run: | ||
| working-directory: shared | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| - uses: actions/setup-node@v7 | ||
| with: | ||
| node-version: "20" | ||
| cache: "npm" | ||
| cache-dependency-path: shared/package-lock.json | ||
| - name: Install dependencies | ||
| run: npm install | ||
| - name: Run tests with coverage (thresholds enforced) | ||
| run: npx jest --coverage --ci | ||
| - uses: actions/upload-artifact@v7 | ||
| if: always() | ||
| with: | ||
| name: shared-coverage-report | ||
| path: shared/coverage/ | ||