Implements the plan from the carpenter walkthrough audit: make the project flow -> final review -> PDF path measurable and safe to gate CI on. - projectHydrationService loads a project's geometry/labor/materials/ rentals/packages/calculation in parallel and reports per-resource timing plus the slowest resource, so hydration SLA breaches are visible instead of silent - projectReadiness centralizes the blocking checks final review must pass before a quote can be sent (name, customer, description, materials/labor present, zero-price lines) - pdfSourceSignature detects when a generated PDF is stale relative to the current project data, so a carpenter doesn't send an outdated quote - MaterialLinkReview + new customer-projects match-preview/link endpoints let a user review and confirm suggested master-material matches instead of silently auto-linking - ProjectFlow refactored around the new hydration/readiness utilities; FinalReview surfaces readiness checks and unlinked material counts - CI: the carpenter smoke test (full-roof-quote-flow.spec.js) is now a blocking gate on main, with the rest of the Playwright suite running best-effort alongside it - docs/qa/CARPENTER_WALKTHROUGH_RELEASE_GATE.md documents the gate and the monthly production audit procedure Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
306 lines
8.6 KiB
YAML
306 lines
8.6 KiB
YAML
name: CI - Test & Build
|
|
|
|
on:
|
|
push:
|
|
branches: [ main, develop, 'feature/**', 'claude/**' ]
|
|
pull_request:
|
|
branches: [ main, develop ]
|
|
|
|
env:
|
|
NODE_VERSION: '22'
|
|
|
|
jobs:
|
|
# ============================================
|
|
# Lint & Type Check
|
|
# ============================================
|
|
lint:
|
|
name: Lint & Type Check
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v7
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: 'npm'
|
|
|
|
- name: Install root dependencies
|
|
run: npm ci
|
|
|
|
- name: Install frontend dependencies
|
|
working-directory: frontend
|
|
run: npm ci
|
|
|
|
- name: Install backend dependencies
|
|
working-directory: backend
|
|
run: npm ci
|
|
|
|
- name: Run lint gate
|
|
run: npm run lint
|
|
|
|
# ============================================
|
|
# Backend Unit Tests
|
|
# ============================================
|
|
backend-tests:
|
|
name: Backend Unit Tests
|
|
runs-on: ubuntu-latest
|
|
services:
|
|
mariadb:
|
|
image: mariadb:10.11
|
|
env:
|
|
MYSQL_ROOT_PASSWORD: testpassword
|
|
MYSQL_DATABASE: tilbudgivern_test
|
|
MYSQL_USER: testuser
|
|
MYSQL_PASSWORD: testpassword
|
|
ports:
|
|
- 3306:3306
|
|
options: >-
|
|
--health-cmd="mysqladmin ping -h localhost"
|
|
--health-interval=10s
|
|
--health-timeout=5s
|
|
--health-retries=5
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v7
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: 'npm'
|
|
|
|
- name: Install backend dependencies
|
|
working-directory: backend
|
|
run: npm ci
|
|
|
|
- name: Wait for MariaDB
|
|
run: |
|
|
while ! mysqladmin ping -h"127.0.0.1" --silent; do
|
|
sleep 1
|
|
done
|
|
|
|
- name: Run backend tests
|
|
working-directory: backend
|
|
env:
|
|
DB_HOST: 127.0.0.1
|
|
DB_PORT: 3306
|
|
DB_USER: testuser
|
|
DB_PASSWORD: testpassword
|
|
DB_NAME: tilbudgivern_test
|
|
NODE_ENV: test
|
|
run: npm test -- --coverage --passWithNoTests
|
|
|
|
- name: Upload coverage report
|
|
uses: actions/upload-artifact@v7
|
|
if: always()
|
|
# Artifact upload must never fail the job (storage quota can be full)
|
|
continue-on-error: true
|
|
with:
|
|
name: backend-coverage
|
|
path: backend/coverage
|
|
retention-days: 3
|
|
|
|
# ============================================
|
|
# Frontend Build
|
|
# ============================================
|
|
frontend-build:
|
|
name: Frontend Build
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v7
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: 'npm'
|
|
|
|
- name: Install frontend dependencies
|
|
working-directory: frontend
|
|
run: npm ci
|
|
|
|
- name: Build frontend
|
|
working-directory: frontend
|
|
env:
|
|
CI: false # Prevent treating warnings as errors
|
|
run: npm run build
|
|
|
|
- name: Upload frontend build artifact
|
|
uses: actions/upload-artifact@v7
|
|
# Artifact upload must never fail the job (storage quota can be full)
|
|
continue-on-error: true
|
|
with:
|
|
name: frontend-build
|
|
path: frontend/build
|
|
retention-days: 3
|
|
|
|
# ============================================
|
|
# E2E Tests (Playwright)
|
|
# ============================================
|
|
e2e-tests:
|
|
name: E2E Tests (Playwright)
|
|
runs-on: ubuntu-latest
|
|
needs: [frontend-build, backend-tests]
|
|
if: github.event_name == 'pull_request'
|
|
services:
|
|
mariadb:
|
|
image: mariadb:10.11
|
|
env:
|
|
MYSQL_ROOT_PASSWORD: testpassword
|
|
MYSQL_DATABASE: tilbudgivern_test
|
|
MYSQL_USER: testuser
|
|
MYSQL_PASSWORD: testpassword
|
|
ports:
|
|
- 3306:3306
|
|
options: >-
|
|
--health-cmd="mysqladmin ping -h localhost"
|
|
--health-interval=10s
|
|
--health-timeout=5s
|
|
--health-retries=5
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v7
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: 'npm'
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
npm ci
|
|
cd frontend && npm ci
|
|
cd ../backend && npm ci
|
|
cd ../tests && npm ci
|
|
|
|
# Build in-job instead of downloading the frontend-build artifact -
|
|
# artifact upload is best-effort and can be skipped on full quota
|
|
- name: Build frontend
|
|
working-directory: frontend
|
|
env:
|
|
CI: false
|
|
run: npm run build
|
|
|
|
- name: Install Playwright browsers
|
|
working-directory: tests
|
|
run: npx playwright install --with-deps chromium
|
|
|
|
- name: Wait for MariaDB
|
|
run: |
|
|
while ! mysqladmin ping -h"127.0.0.1" --silent; do
|
|
sleep 1
|
|
done
|
|
|
|
- name: Start server
|
|
env:
|
|
DB_HOST: 127.0.0.1
|
|
DB_PORT: 3306
|
|
DB_USER: testuser
|
|
DB_PASSWORD: testpassword
|
|
DB_NAME: tilbudgivern_test
|
|
PORT: 4032
|
|
NODE_ENV: test
|
|
JWT_ACCESS_SECRET: ci-only-access-secret-not-for-production
|
|
JWT_REFRESH_SECRET: ci-only-refresh-secret-not-for-production
|
|
AUTH_USERNAME: ci-test-user
|
|
AUTH_PASSWORD: ci-test-password
|
|
ORDRESTYRING_API_TOKEN: ci-test-token-no-network-use
|
|
ORDRESTYRING_AUTO_SYNC_DISABLED: true
|
|
run: |
|
|
cd backend && node unified-server.js &
|
|
sleep 10
|
|
curl -f http://localhost:4032/api/health || exit 1
|
|
|
|
- name: Run blocking carpenter smoke test
|
|
working-directory: tests
|
|
env:
|
|
PLAYWRIGHT_BASE_URL: http://localhost:4032
|
|
PLAYWRIGHT_USERNAME: ci-test-user
|
|
PLAYWRIGHT_PASSWORD: ci-test-password
|
|
run: npx playwright test full-roof-quote-flow.spec.js --project=chromium --reporter=list
|
|
|
|
- name: Run extended Playwright tests
|
|
working-directory: tests
|
|
env:
|
|
PLAYWRIGHT_BASE_URL: http://localhost:4032
|
|
PLAYWRIGHT_USERNAME: ci-test-user
|
|
PLAYWRIGHT_PASSWORD: ci-test-password
|
|
run: npx playwright test --project=chromium --reporter=html --grep-invert "full carpenter roof quote smoke flow"
|
|
continue-on-error: true
|
|
|
|
- name: Upload Playwright report
|
|
uses: actions/upload-artifact@v7
|
|
if: always()
|
|
continue-on-error: true
|
|
with:
|
|
name: playwright-report
|
|
path: tests/playwright-report
|
|
retention-days: 3
|
|
|
|
- name: Upload Playwright screenshots
|
|
uses: actions/upload-artifact@v7
|
|
if: failure()
|
|
continue-on-error: true
|
|
with:
|
|
name: playwright-screenshots
|
|
path: tests/test-results
|
|
retention-days: 3
|
|
|
|
# ============================================
|
|
# Security Scan
|
|
# ============================================
|
|
security-scan:
|
|
name: Security Scan
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v7
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: 'npm'
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
npm ci
|
|
cd frontend && npm ci
|
|
cd ../backend && npm ci
|
|
|
|
- name: Run npm audit (frontend)
|
|
working-directory: frontend
|
|
run: npm audit --audit-level=high
|
|
continue-on-error: true
|
|
|
|
- name: Run npm audit (backend)
|
|
working-directory: backend
|
|
run: npm audit --audit-level=high
|
|
continue-on-error: true
|
|
|
|
# ============================================
|
|
# Summary Job
|
|
# ============================================
|
|
ci-summary:
|
|
name: CI Summary
|
|
runs-on: ubuntu-latest
|
|
needs: [lint, backend-tests, frontend-build, security-scan, e2e-tests]
|
|
if: always()
|
|
steps:
|
|
- name: Check CI status
|
|
run: |
|
|
if [[ "${{ needs.lint.result }}" == "failure" ]] || \
|
|
[[ "${{ needs.backend-tests.result }}" == "failure" ]] || \
|
|
[[ "${{ needs.frontend-build.result }}" == "failure" ]] || \
|
|
[[ "${{ needs.e2e-tests.result }}" == "failure" ]]; then
|
|
echo "CI failed!"
|
|
exit 1
|
|
fi
|
|
echo "CI passed successfully!"
|