name: Node.js CI on: workflow_call: permissions: contents: read pull-requests: write issues: write # All jobs run in parallel for faster CI execution # Using pnpm install --frozen-lockfile for reproducible installs # Build already uses esbuild for fast TypeScript compilation jobs: lint: name: Lint TypeScript/JavaScript Code runs-on: blacksmith-8vcpu-ubuntu-2404-arm env: GITHUB_REPO_NAME: ${{ github.repository }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '24' - name: Setup pnpm uses: pnpm/action-setup@v2 with: version: 9 run_install: false - name: Get pnpm store directory shell: bash run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Setup pnpm cache uses: useblacksmith/cache@v5 continue-on-error: true with: path: ${{ env.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('web/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - name: Install system dependencies run: | sudo apt-get update sudo apt-get install -y libpam0g-dev - name: Install dependencies working-directory: web run: pnpm install --frozen-lockfile - name: Check formatting with Biome id: biome-format working-directory: web continue-on-error: true run: | pnpm run format:check 2>&1 | tee biome-format-output.txt echo "result=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT - name: Run Biome linting id: biome-lint working-directory: web continue-on-error: true run: | pnpm run lint:biome 2>&1 | tee biome-lint-output.txt echo "result=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT - name: Read Biome Format Output if: always() id: biome-format-output working-directory: web run: | if [ -f biome-format-output.txt ]; then echo 'content<> $GITHUB_OUTPUT cat biome-format-output.txt >> $GITHUB_OUTPUT echo 'EOF' >> $GITHUB_OUTPUT else echo "content=No output" >> $GITHUB_OUTPUT fi - name: Read Biome Lint Output if: always() id: biome-lint-output working-directory: web run: | if [ -f biome-lint-output.txt ]; then echo 'content<> $GITHUB_OUTPUT cat biome-lint-output.txt >> $GITHUB_OUTPUT echo 'EOF' >> $GITHUB_OUTPUT else echo "content=No output" >> $GITHUB_OUTPUT fi - name: Report Biome Format Results if: always() uses: ./.github/actions/lint-reporter with: title: 'Node.js Biome Formatting' lint-result: ${{ steps.biome-format.outputs.result == '0' && 'success' || 'failure' }} lint-output: ${{ steps.biome-format-output.outputs.content }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Report Biome Lint Results if: always() uses: ./.github/actions/lint-reporter with: title: 'Node.js Biome Linting' lint-result: ${{ steps.biome-lint.outputs.result == '0' && 'success' || 'failure' }} lint-output: ${{ steps.biome-lint-output.outputs.content }} github-token: ${{ secrets.GITHUB_TOKEN }} build-and-test: name: Build and Test runs-on: blacksmith-8vcpu-ubuntu-2404-arm env: GITHUB_REPO_NAME: ${{ github.repository }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '24' - name: Setup pnpm uses: pnpm/action-setup@v2 with: version: 9 run_install: false - name: Get pnpm store directory shell: bash run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Setup pnpm cache uses: useblacksmith/cache@v5 continue-on-error: true with: path: ${{ env.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('web/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - name: Install system dependencies run: | sudo apt-get update sudo apt-get install -y libpam0g-dev - name: Cache TypeScript build info uses: useblacksmith/cache@v5 continue-on-error: true with: path: | web/dist/tsconfig.server.tsbuildinfo web/public/tsconfig.client.tsbuildinfo web/public/tsconfig.sw.tsbuildinfo key: ${{ runner.os }}-tsbuild-${{ hashFiles('web/src/**/*.ts', 'web/tsconfig*.json') }} restore-keys: | ${{ runner.os }}-tsbuild- - name: Install dependencies working-directory: web run: pnpm install --frozen-lockfile - name: Build frontend and backend working-directory: web run: pnpm run build:ci - name: Run client tests with coverage id: test-client-coverage working-directory: web run: | pnpm run test:client:coverage 2>&1 | tee test-client-output.txt echo "result=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT - name: Run server tests with coverage id: test-server-coverage working-directory: web run: | pnpm run test:server:coverage 2>&1 | tee test-server-output.txt echo "result=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT env: CI: true - name: Check test results if: always() working-directory: web run: | if [ "${{ steps.test-client-coverage.outputs.result }}" != "0" ] || [ "${{ steps.test-server-coverage.outputs.result }}" != "0" ]; then echo "::error::Tests failed" exit 1 fi - name: Generate coverage summaries if: always() working-directory: web run: | # Process client coverage if [ -f coverage/client/coverage-summary.json ]; then node -e " const coverage = require('./coverage/client/coverage-summary.json'); const total = coverage.total; const summary = { type: 'client', lines: { pct: total.lines.pct, covered: total.lines.covered, total: total.lines.total }, statements: { pct: total.statements.pct, covered: total.statements.covered, total: total.statements.total }, functions: { pct: total.functions.pct, covered: total.functions.covered, total: total.functions.total }, branches: { pct: total.branches.pct, covered: total.branches.covered, total: total.branches.total } }; console.log(JSON.stringify(summary, null, 2)); " > coverage-client-summary.json if [ -f test-client-output.txt ]; then tail -n 50 test-client-output.txt > coverage-client-output.txt fi else echo '{"error": "No client coverage data found"}' > coverage-client-summary.json fi # Process server coverage if [ -f coverage/server/coverage-summary.json ]; then node -e " const coverage = require('./coverage/server/coverage-summary.json'); const total = coverage.total; const summary = { type: 'server', lines: { pct: total.lines.pct, covered: total.lines.covered, total: total.lines.total }, statements: { pct: total.statements.pct, covered: total.statements.covered, total: total.statements.total }, functions: { pct: total.functions.pct, covered: total.functions.covered, total: total.functions.total }, branches: { pct: total.branches.pct, covered: total.branches.covered, total: total.branches.total } }; console.log(JSON.stringify(summary, null, 2)); " > coverage-server-summary.json if [ -f test-server-output.txt ]; then tail -n 50 test-server-output.txt > coverage-server-output.txt fi else echo '{"error": "No server coverage data found"}' > coverage-server-summary.json fi # Create combined summary for backward compatibility node -e " const clientCov = require('./coverage-client-summary.json'); const serverCov = require('./coverage-server-summary.json'); const combined = { client: clientCov, server: serverCov }; console.log(JSON.stringify(combined, null, 2)); " > coverage-summary-formatted.json || echo '{"error": "Failed to combine coverage data"}' > coverage-summary-formatted.json - name: Upload coverage artifacts if: always() uses: actions/upload-artifact@v4 with: name: node-coverage path: | web/coverage-summary-formatted.json web/coverage-client-summary.json web/coverage-server-summary.json web/coverage-client-output.txt web/coverage-server-output.txt web/coverage/client/lcov.info web/coverage/server/lcov.info - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: web-build-${{ github.sha }} path: | web/dist/ web/public/bundle/ retention-days: 1 type-check: name: TypeScript Type Checking runs-on: blacksmith-8vcpu-ubuntu-2404-arm env: GITHUB_REPO_NAME: ${{ github.repository }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '24' - name: Setup pnpm uses: pnpm/action-setup@v2 with: version: 9 run_install: false - name: Get pnpm store directory shell: bash run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Setup pnpm cache uses: useblacksmith/cache@v5 continue-on-error: true with: path: ${{ env.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('web/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - name: Install system dependencies run: | sudo apt-get update sudo apt-get install -y libpam0g-dev - name: Install dependencies working-directory: web run: pnpm install --frozen-lockfile - name: Check TypeScript types working-directory: web run: pnpm run typecheck audit: name: Security Audit runs-on: blacksmith-8vcpu-ubuntu-2404-arm steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '24' - name: Setup pnpm uses: pnpm/action-setup@v2 with: version: 9 run_install: false - name: Run pnpm audit working-directory: web run: pnpm audit --audit-level=moderate || true # || true to not fail the build on vulnerabilities, but still report them report-coverage: name: Report Coverage Results runs-on: blacksmith-8vcpu-ubuntu-2404-arm needs: [build-and-test] if: always() && github.event_name == 'pull_request' steps: - name: Checkout code uses: actions/checkout@v4 - name: Download coverage artifacts uses: actions/download-artifact@v4 with: name: node-coverage path: web/coverage-artifacts - name: Read coverage summaries id: coverage working-directory: web run: | # Initialize result variables CLIENT_RESULT="failure" SERVER_RESULT="failure" # Process client coverage if [ -f coverage-artifacts/coverage-client-summary.json ]; then CLIENT_JSON=$(cat coverage-artifacts/coverage-client-summary.json) CLIENT_LINES=$(echo "$CLIENT_JSON" | jq -r '.lines.pct // 0') CLIENT_FUNCTIONS=$(echo "$CLIENT_JSON" | jq -r '.functions.pct // 0') CLIENT_BRANCHES=$(echo "$CLIENT_JSON" | jq -r '.branches.pct // 0') CLIENT_STATEMENTS=$(echo "$CLIENT_JSON" | jq -r '.statements.pct // 0') # Always report as success - we're just reporting coverage CLIENT_RESULT="success" echo "client_lines=$CLIENT_LINES" >> $GITHUB_OUTPUT echo "client_functions=$CLIENT_FUNCTIONS" >> $GITHUB_OUTPUT echo "client_branches=$CLIENT_BRANCHES" >> $GITHUB_OUTPUT echo "client_statements=$CLIENT_STATEMENTS" >> $GITHUB_OUTPUT fi # Process server coverage if [ -f coverage-artifacts/coverage-server-summary.json ]; then SERVER_JSON=$(cat coverage-artifacts/coverage-server-summary.json) SERVER_LINES=$(echo "$SERVER_JSON" | jq -r '.lines.pct // 0') SERVER_FUNCTIONS=$(echo "$SERVER_JSON" | jq -r '.functions.pct // 0') SERVER_BRANCHES=$(echo "$SERVER_JSON" | jq -r '.branches.pct // 0') SERVER_STATEMENTS=$(echo "$SERVER_JSON" | jq -r '.statements.pct // 0') # Always report as success - we're just reporting coverage SERVER_RESULT="success" echo "server_lines=$SERVER_LINES" >> $GITHUB_OUTPUT echo "server_functions=$SERVER_FUNCTIONS" >> $GITHUB_OUTPUT echo "server_branches=$SERVER_BRANCHES" >> $GITHUB_OUTPUT echo "server_statements=$SERVER_STATEMENTS" >> $GITHUB_OUTPUT fi # Always report as success - we're just reporting coverage echo "result=success" >> $GITHUB_OUTPUT echo "client_result=$CLIENT_RESULT" >> $GITHUB_OUTPUT echo "server_result=$SERVER_RESULT" >> $GITHUB_OUTPUT # Format output CLIENT_OUTPUT="" SERVER_OUTPUT="" if [ -f coverage-artifacts/coverage-client-output.txt ]; then CLIENT_OUTPUT=$(tail -n 20 coverage-artifacts/coverage-client-output.txt | grep -v "^\[" | head -10) fi if [ -f coverage-artifacts/coverage-server-output.txt ]; then SERVER_OUTPUT=$(tail -n 20 coverage-artifacts/coverage-server-output.txt | grep -v "^\[" | head -10) fi echo "client_output<> $GITHUB_OUTPUT echo "$CLIENT_OUTPUT" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT echo "server_output<> $GITHUB_OUTPUT echo "$SERVER_OUTPUT" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Format coverage output id: format-coverage if: always() run: | # Format client coverage CLIENT_OUTPUT="**Client Coverage:**\n" if [ "${{ steps.coverage.outputs.client_lines }}" != "" ]; then CLIENT_LINES="${{ steps.coverage.outputs.client_lines }}" CLIENT_FUNCTIONS="${{ steps.coverage.outputs.client_functions }}" CLIENT_BRANCHES="${{ steps.coverage.outputs.client_branches }}" CLIENT_STATEMENTS="${{ steps.coverage.outputs.client_statements }}" CLIENT_OUTPUT="${CLIENT_OUTPUT}• Lines: ${CLIENT_LINES}%\n" CLIENT_OUTPUT="${CLIENT_OUTPUT}• Functions: ${CLIENT_FUNCTIONS}%\n" CLIENT_OUTPUT="${CLIENT_OUTPUT}• Branches: ${CLIENT_BRANCHES}%\n" CLIENT_OUTPUT="${CLIENT_OUTPUT}• Statements: ${CLIENT_STATEMENTS}%\n" else CLIENT_OUTPUT="${CLIENT_OUTPUT}No client coverage data found\n" fi # Format server coverage SERVER_OUTPUT="\n**Server Coverage:**\n" if [ "${{ steps.coverage.outputs.server_lines }}" != "" ]; then SERVER_LINES="${{ steps.coverage.outputs.server_lines }}" SERVER_FUNCTIONS="${{ steps.coverage.outputs.server_functions }}" SERVER_BRANCHES="${{ steps.coverage.outputs.server_branches }}" SERVER_STATEMENTS="${{ steps.coverage.outputs.server_statements }}" SERVER_OUTPUT="${SERVER_OUTPUT}• Lines: ${SERVER_LINES}%\n" SERVER_OUTPUT="${SERVER_OUTPUT}• Functions: ${SERVER_FUNCTIONS}%\n" SERVER_OUTPUT="${SERVER_OUTPUT}• Branches: ${SERVER_BRANCHES}%\n" SERVER_OUTPUT="${SERVER_OUTPUT}• Statements: ${SERVER_STATEMENTS}%" else SERVER_OUTPUT="${SERVER_OUTPUT}No server coverage data found" fi echo "output<> $GITHUB_OUTPUT echo -e "${CLIENT_OUTPUT}${SERVER_OUTPUT}" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Report Coverage Results uses: ./.github/actions/lint-reporter with: title: 'Node.js Test Coverage' lint-result: ${{ steps.coverage.outputs.result }} lint-output: ${{ steps.format-coverage.outputs.output }} github-token: ${{ secrets.GITHUB_TOKEN }}