From 234330e48204c944e47ce165998404d6adde236e Mon Sep 17 00:00:00 2001 From: Thom Lamb Date: Thu, 13 Aug 2026 19:10:30 -0500 Subject: [PATCH] Add CI test workflow with job summaries --- .gitea/workflows/tests.yml | 164 ++++++++++++++++++++++++ backend/package.json | 4 +- backend/scripts/summarize-jest.js | 43 +++++++ backend/scripts/summarize-playwright.js | 54 ++++++++ 4 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 .gitea/workflows/tests.yml create mode 100644 backend/scripts/summarize-jest.js create mode 100644 backend/scripts/summarize-playwright.js diff --git a/.gitea/workflows/tests.yml b/.gitea/workflows/tests.yml new file mode 100644 index 0000000..3f547bc --- /dev/null +++ b/.gitea/workflows/tests.yml @@ -0,0 +1,164 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + +jobs: + backend-unit: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install deps + run: npm install + working-directory: backend + + - name: Run unit tests + id: unit + continue-on-error: true + run: npm run test:unit:json + working-directory: backend + + - name: Summarize + if: always() + run: node scripts/summarize-jest.js backend/unit-results.json "Backend Unit Test" + + - name: Fail job if tests failed + if: steps.unit.outcome == 'failure' + run: exit 1 + + backend-integration: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: redefined_test + POSTGRES_PASSWORD: redefined_test + POSTGRES_DB: redefined_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U redefined_test" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + env: + TEST_PGHOST: postgres + TEST_PGPORT: 5432 + TEST_PGUSER: redefined_test + TEST_PGPASSWORD: redefined_test + TEST_PGDATABASE: redefined_test + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install deps + run: npm install + working-directory: backend + + - name: Run integration tests + id: integration + continue-on-error: true + run: npm run test:integration:json + working-directory: backend + + - name: Summarize + if: always() + run: node scripts/summarize-jest.js backend/integration-results.json "Backend Integration Test" + + - name: Fail job if tests failed + if: steps.integration.outcome == 'failure' + run: exit 1 + + frontend-e2e: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: redefined_test + POSTGRES_PASSWORD: redefined_test + POSTGRES_DB: redefined_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U redefined_test" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + env: + PGHOST: postgres + PGPORT: 5432 + PGUSER: redefined_test + PGPASSWORD: redefined_test + PGDATABASE: redefined_test + PORT: 3000 + DEMO_MODE: 'true' + UPLOADS_DIR: /tmp/redefined-uploads + NODE_ENV: production + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install postgresql-client + run: apt-get update && apt-get install -y postgresql-client + + - name: Load schema + run: PGPASSWORD=redefined_test psql -h postgres -U redefined_test -d redefined_test -f backend/init.sql + + - name: Install backend deps + run: npm install + working-directory: backend + + - name: Build and start backend + run: | + mkdir -p /tmp/redefined-uploads + npm run build + node dist/server.js & + sleep 3 + working-directory: backend + + - name: Install frontend deps + run: npm install + working-directory: frontend + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + working-directory: frontend + + - name: Run Playwright tests + id: e2e + continue-on-error: true + env: + PLAYWRIGHT_JSON_OUTPUT_NAME: playwright-results.json + run: npx playwright test --reporter=json + working-directory: frontend + + - name: Summarize + if: always() + run: node scripts/summarize-playwright.js frontend/playwright-results.json + + - name: Fail job if tests failed + if: steps.e2e.outcome == 'failure' + run: exit 1 \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index bee06e5..2f2259c 100755 --- a/backend/package.json +++ b/backend/package.json @@ -9,7 +9,9 @@ "dev": "tsx watch src/server.ts", "test": "npm run test:unit", "test:unit": "jest -c jest.unit.config.js", + "test:unit:json": "jest -c jest.unit.config.js --json --outputFile=unit-results.json", "test:integration": "jest -c jest.integration.config.js --runInBand", + "test:integration:json": "jest -c jest.integration.config.js --runInBand --json --outputFile=integration-results.json", "db:test:up": "docker compose -f docker-compose.test.yml up -d", "db:test:down": "docker compose -f docker-compose.test.yml down -v" }, @@ -37,4 +39,4 @@ "supertest": "^7.0.0", "@types/supertest": "^6.0.2" } -} +} \ No newline at end of file diff --git a/backend/scripts/summarize-jest.js b/backend/scripts/summarize-jest.js new file mode 100644 index 0000000..eac7e38 --- /dev/null +++ b/backend/scripts/summarize-jest.js @@ -0,0 +1,43 @@ +#!/usr/bin/env node +const fs = require('fs'); + +const [, , resultsPath, label] = process.argv; +const raw = fs.readFileSync(resultsPath, 'utf-8'); +const data = JSON.parse(raw); + +const lines = []; +lines.push(`## ${label || 'Test'} Results`); +lines.push(''); +lines.push('| | Count |'); +lines.push('|---|---|'); +lines.push(`| ✅ Passed | ${data.numPassedTests} |`); +lines.push(`| ❌ Failed | ${data.numFailedTests} |`); +lines.push(`| ⏭️ Skipped | ${data.numPendingTests} |`); +lines.push(`| **Total** | **${data.numTotalTests}** |`); +lines.push(''); + +if (data.numFailedTests > 0) { + lines.push('### Failures'); + lines.push(''); + for (const suite of data.testResults) { + for (const t of suite.testResults) { + if (t.status === 'failed') { + lines.push(`- **${t.fullName}**`); + const msg = t.failureMessages && t.failureMessages[0] + ? t.failureMessages[0].split('\n')[0] + : 'see job log for details'; + lines.push(` \`${msg}\``); + } + } + } + lines.push(''); +} + +const summaryPath = process.env.GITEA_STEP_SUMMARY || process.env.GITHUB_STEP_SUMMARY; +if (summaryPath) { + fs.appendFileSync(summaryPath, lines.join('\n') + '\n'); +} else { + console.log(lines.join('\n')); +} + +process.exit(data.numFailedTests > 0 ? 1 : 0); \ No newline at end of file diff --git a/backend/scripts/summarize-playwright.js b/backend/scripts/summarize-playwright.js new file mode 100644 index 0000000..2a191a8 --- /dev/null +++ b/backend/scripts/summarize-playwright.js @@ -0,0 +1,54 @@ +#!/usr/bin/env node +const fs = require('fs'); + +const [, , resultsPath] = process.argv; +const raw = fs.readFileSync(resultsPath, 'utf-8'); +const data = JSON.parse(raw); +const stats = data.stats || {}; + +const lines = []; +lines.push('## Playwright E2E Results'); +lines.push(''); +lines.push('| | Count |'); +lines.push('|---|---|'); +lines.push(`| ✅ Passed | ${stats.expected || 0} |`); +lines.push(`| ❌ Failed | ${stats.unexpected || 0} |`); +lines.push(`| ⚠️ Flaky | ${stats.flaky || 0} |`); +lines.push(`| ⏭️ Skipped | ${stats.skipped || 0} |`); +lines.push(''); + +function collectFailures(suites, path) { + path = path || []; + let failures = []; + for (const suite of suites || []) { + const currentPath = path.concat(suite.title).filter(Boolean); + for (const spec of suite.specs || []) { + for (const test of spec.tests || []) { + for (const result of test.results || []) { + if (result.status !== 'passed' && result.status !== 'skipped') { + failures.push(`${currentPath.join(' > ')} > ${spec.title}: ${result.status}`); + } + } + } + } + if (suite.suites) failures = failures.concat(collectFailures(suite.suites, currentPath)); + } + return failures; +} + +const failures = collectFailures(data.suites); +if (failures.length) { + lines.push('### Failures'); + lines.push(''); + for (const f of failures) lines.push(`- ${f}`); + lines.push(''); +} + +const summaryPath = process.env.GITEA_STEP_SUMMARY || process.env.GITHUB_STEP_SUMMARY; +if (summaryPath) { + fs.appendFileSync(summaryPath, lines.join('\n') + '\n'); +} else { + console.log(lines.join('\n')); +} + +process.exit((stats.unexpected || 0) > 0 ? 1 : 0); \ No newline at end of file