diff --git a/.github/workflows/quality-gate.yml b/.github/workflows/quality-gate.yml new file mode 100644 index 00000000..a2eb43ad --- /dev/null +++ b/.github/workflows/quality-gate.yml @@ -0,0 +1,166 @@ +name: Quality Gate + +on: + pull_request: + branches: [main] + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: quality-gate-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + changes: + name: Detect changed areas + runs-on: ubuntu-latest + outputs: + frontend: ${{ steps.changed.outputs.frontend }} + backend: ${{ steps.changed.outputs.backend }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Detect frontend and backend changes + id: changed + env: + EVENT_NAME: ${{ github.event_name }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PUSH_BASE_SHA: ${{ github.event.before }} + PUSH_HEAD_SHA: ${{ github.sha }} + run: | + set -euo pipefail + + if [ "$EVENT_NAME" = "pull_request" ]; then + base_sha="$PR_BASE_SHA" + head_sha="$PR_HEAD_SHA" + elif [ "$EVENT_NAME" = "push" ] && [ -n "$PUSH_BASE_SHA" ]; then + base_sha="$PUSH_BASE_SHA" + head_sha="$PUSH_HEAD_SHA" + else + base_sha="" + head_sha="" + fi + + if [ -n "$base_sha" ] && [ "$base_sha" != "0000000000000000000000000000000000000000" ]; then + changed_files=$(git diff --name-only "$base_sha" "$head_sha") + else + changed_files=$(git ls-files) + fi + + frontend=false + backend=false + + if printf '%s\n' "$changed_files" | grep -Eq '^frontend/'; then + frontend=true + fi + if printf '%s\n' "$changed_files" | grep -Eq '^backend/'; then + backend=true + fi + + { + echo "frontend=$frontend" + echo "backend=$backend" + } >> "$GITHUB_OUTPUT" + + echo "frontend=$frontend backend=$backend" + + frontend: + name: Frontend checks + needs: changes + if: needs.changes.outputs.frontend == 'true' + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + CI: 'true' + defaults: + run: + working-directory: frontend + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Check formatting + run: npm run format:check + + - name: Lint + run: npm run lint + + - name: Typecheck + run: npm run typecheck + + - name: Test + run: npm run test + + - name: Build + run: npm run build + + backend: + name: Backend checks + needs: changes + if: needs.changes.outputs.backend == 'true' + runs-on: ubuntu-latest + timeout-minutes: 15 + defaults: + run: + working-directory: backend + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v8.3.2 + with: + enable-cache: true + + - name: Setup Python + run: uv python install 3.12 + + - name: Install dependencies + run: uv sync --frozen + + - name: Ruff + run: uv run ruff check . + + - name: Import-linter + run: uv run lint-imports + + - name: Pytest + run: uv run pytest -q + + quality-gate: + name: Quality gate + if: always() + needs: [changes, frontend, backend] + runs-on: ubuntu-latest + steps: + - name: Require all relevant checks + env: + CHANGES_RESULT: ${{ needs.changes.result }} + FRONTEND_RESULT: ${{ needs.frontend.result }} + BACKEND_RESULT: ${{ needs.backend.result }} + run: | + set -euo pipefail + for result in "$CHANGES_RESULT" "$FRONTEND_RESULT" "$BACKEND_RESULT"; do + case "$result" in + success|skipped) ;; + *) echo "::error::A required CI job ended with result: $result"; exit 1 ;; + esac + done diff --git a/vercel.json b/vercel.json new file mode 100644 index 00000000..b61f0735 --- /dev/null +++ b/vercel.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": "vite", + "installCommand": "npm --prefix frontend ci", + "buildCommand": "npm --prefix frontend run build", + "outputDirectory": "frontend/dist", + "ignoreCommand": "test ! -d frontend || git diff --quiet HEAD^ HEAD -- frontend/ vercel.json", + "rewrites": [ + { + "source": "/(.*)", + "destination": "/index.html" + } + ] +}