Skip to content

Security: harden Content-Security-Policy and verify report pipeline #3

Security: harden Content-Security-Policy and verify report pipeline

Security: harden Content-Security-Policy and verify report pipeline #3

Workflow file for this run

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 |

Check failure on line 151 in .github/workflows/test.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/test.yml

Invalid workflow file

You have an error in your yaml syntax on line 151
|--------|----------|-----------|--------|
| 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/