diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d1758c0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,111 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + NODE_VERSION: 22 + PNPM_VERSION: 10.28.0 + +jobs: + quality: + name: Quality Gate + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm lint + + - name: Typecheck + run: pnpm typecheck + + - name: Web tests + run: pnpm --filter web test + + - name: Build + run: pnpm build + + server-test: + name: Server Tests + runs-on: ubuntu-latest + timeout-minutes: 30 + services: + postgres: + image: postgres:16 + env: + POSTGRES_DB: loreo_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres -d loreo_test" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + NODE_ENV: test + DATABASE_USER: postgres + DATABASE_PASSWORD: postgres + DATABASE_DB: loreo_test + DATABASE_HOST: 127.0.0.1 + DATABASE_PORT: 5432 + DATABASE_POOL_MAX: 10 + REDIS_HOST: 127.0.0.1 + REDIS_PORT: 6379 + CORS_ORIGINS: http://localhost:3001 + JWT_SECRET: test-jwt-secret-at-least-32-characters + PUBLIC_URL: http://localhost:3000 + STORAGE_PROVIDER: local + STORAGE_PATH: ./data/storage-test + BROWSER_URL: ws://127.0.0.1:4444/camoufox + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Server tests + run: pnpm --filter server test diff --git a/.github/workflows/docker-browser-publish.yml b/.github/workflows/docker-browser-publish.yml index 170f4d8..1653891 100644 --- a/.github/workflows/docker-browser-publish.yml +++ b/.github/workflows/docker-browser-publish.yml @@ -1,12 +1,6 @@ name: Docker Publish Browser on: - pull_request: - branches: [main] - paths: - - .github/workflows/docker-browser-publish.yml - - apps/server/Dockerfile.browser - - apps/server/browser/** push: branches: [main] tags: ['v*.*.*'] @@ -43,7 +37,6 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Log in to GHCR - if: github.event_name != 'pull_request' timeout-minutes: 5 uses: docker/login-action@v3 with: @@ -58,7 +51,6 @@ jobs: images: ${{ env.REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.IMAGE_NAME }} tags: | type=ref,event=branch - type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha,prefix=sha- @@ -70,7 +62,7 @@ jobs: context: . file: apps/server/Dockerfile.browser platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} + push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha,scope=${{ env.IMAGE_NAME }} diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 3fbea3c..2727d3a 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -1,16 +1,6 @@ name: Docker Publish Apps on: - pull_request: - branches: [main] - paths: - - .github/workflows/docker-publish.yml - - apps/** - - '!apps/server/Dockerfile.browser' - - packages/** - - package.json - - pnpm-lock.yaml - - pnpm-workspace.yaml push: branches: [main] tags: ['v*.*.*'] @@ -61,7 +51,6 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Log in to GHCR - if: github.event_name != 'pull_request' timeout-minutes: 5 uses: docker/login-action@v3 with: @@ -76,7 +65,6 @@ jobs: images: ${{ env.REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ matrix.image }} tags: | type=ref,event=branch - type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha,prefix=sha- @@ -88,7 +76,7 @@ jobs: context: . file: ${{ matrix.dockerfile }} platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} + push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: ${{ matrix.build-args }} diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 5018be8..cab7141 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -1,6 +1,6 @@ { "$schema": "./node_modules/oxfmt/configuration_schema.json", - "ignorePatterns": [], + "ignorePatterns": ["apps/web/src/routeTree.gen.ts"], "semi": true, "singleQuote": true, "trailingComma": "none", diff --git a/apps/server/browser/server.js b/apps/server/browser/server.js index 706be9a..5d1351c 100644 --- a/apps/server/browser/server.js +++ b/apps/server/browser/server.js @@ -1,41 +1,41 @@ -import { launchOptions } from "camoufox-js"; -import { firefox } from "playwright-core"; +import { launchOptions } from 'camoufox-js'; +import { firefox } from 'playwright-core'; -const PORT = parseInt(process.env.PORT || "4444", 10); -const WS_PATH = process.env.WS_PATH || "/camoufox"; +const PORT = parseInt(process.env.PORT || '4444', 10); +const WS_PATH = process.env.WS_PATH || '/camoufox'; async function main() { - const options = await launchOptions({ - headless: true, - os: "linux", - firefox_user_prefs: { - "browser.sessionhistory.max_entries": 0, - "browser.sessionhistory.max_total_viewers": 0, - "javascript.options.mem.gc_incremental_slice_ms": 10, - }, - }); + const options = await launchOptions({ + headless: true, + os: 'linux', + firefox_user_prefs: { + 'browser.sessionhistory.max_entries': 0, + 'browser.sessionhistory.max_total_viewers': 0, + 'javascript.options.mem.gc_incremental_slice_ms': 10 + } + }); - const server = await firefox.launchServer({ - ...options, - host: "0.0.0.0", - port: PORT, - wsPath: WS_PATH, - }); + const server = await firefox.launchServer({ + ...options, + host: '0.0.0.0', + port: PORT, + wsPath: WS_PATH + }); - console.log(`Browser server listening on port ${PORT}`); - console.log(`WebSocket endpoint: ${server.wsEndpoint()}`); + console.log(`Browser server listening on port ${PORT}`); + console.log(`WebSocket endpoint: ${server.wsEndpoint()}`); - const shutdown = async () => { - console.log("Shutting down..."); - await server.close(); - process.exit(0); - }; + const shutdown = async () => { + console.log('Shutting down...'); + await server.close(); + process.exit(0); + }; - process.on("SIGINT", shutdown); - process.on("SIGTERM", shutdown); + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); } main().catch((err) => { - console.error("Failed to start browser server:", err); - process.exit(1); + console.error('Failed to start browser server:', err); + process.exit(1); }); diff --git a/apps/web/src/app.test.tsx b/apps/web/src/app.test.tsx deleted file mode 100644 index a06071d..0000000 --- a/apps/web/src/app.test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { HttpResponse, http } from 'msw'; -import { describe, expect, it } from 'vitest'; - -import { App } from './app'; -import { server } from './tests/mocks/server'; - -describe('App', () => { - it('renders the login shell when auth lookup fails', async () => { - window.history.pushState({}, '', '/'); - - server.use( - http.get('http://localhost:3000/auth/user', () => { - return new HttpResponse(null, { status: 401 }); - }) - ); - - render(); - - expect(await screen.findByRole('heading', { name: /welcome back/i })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument(); - expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); - expect(screen.getByLabelText(/password/i)).toBeInTheDocument(); - }); -}); diff --git a/apps/web/src/typography.css b/apps/web/src/typography.css index 5ba09d9..9957a6b 100644 --- a/apps/web/src/typography.css +++ b/apps/web/src/typography.css @@ -66,7 +66,7 @@ font-size: var(--text-sm); line-height: 2; - *:where(:not(.not-prose, .not-prose *))+*:where(:not(.not-prose, .not-prose *)) { + *:where(:not(.not-prose, .not-prose *)) + *:where(:not(.not-prose, .not-prose *)) { margin-top: calc(var(--spacing) * 6); } @@ -98,7 +98,7 @@ margin-top: calc(var(--spacing) * 8); } - h2+h3:where(:not(.not-prose, .not-prose *)) { + h2 + h3:where(:not(.not-prose, .not-prose *)) { margin-top: calc(var(--spacing) * 6); } @@ -127,7 +127,7 @@ padding-left: calc(var(--spacing) * 3); } - ol li+li:where(:not(.not-prose, .not-prose *)) { + ol li + li:where(:not(.not-prose, .not-prose *)) { margin-top: calc(var(--spacing) * 4); } @@ -145,7 +145,7 @@ padding-left: calc(var(--spacing) * 3); } - ul li+li:where(:not(.not-prose, .not-prose *)) { + ul li + li:where(:not(.not-prose, .not-prose *)) { margin-top: calc(var(--spacing) * 4); } @@ -211,7 +211,7 @@ scrollbar-width: thin; } - pre code *+*:where(:not(.not-prose, .not-prose *)) { + pre code * + *:where(:not(.not-prose, .not-prose *)) { margin-top: 0; } @@ -311,7 +311,7 @@ border-color: var(--prose-hr-color); margin-block: --spacing(16); - &+h2 { + & + h2 { margin-top: --spacing(16); } } @@ -349,4 +349,4 @@ :last-child:where(:not(.not-prose, .not-prose *)) { margin-bottom: calc(var(--spacing) * 6); } -} \ No newline at end of file +}