name: CI on: pull_request: paths-ignore: - '**/*.md' - 'docs/**' - '.github/ISSUE_TEMPLATE/**' - '.github/pull_request_template.md' - 'AGENTS.md' - 'CODE_OF_CONDUCT.md' - 'CONTRIBUTING.md' - 'LICENSE' - 'SECURITY.md' - 'SUPPORT.md' push: branches: - main paths-ignore: - '**/*.md' - 'docs/**' - '.github/ISSUE_TEMPLATE/**' - '.github/pull_request_template.md' - 'AGENTS.md' - 'CODE_OF_CONDUCT.md' - 'CONTRIBUTING.md' - 'LICENSE' - 'SECURITY.md' - 'SUPPORT.md' workflow_dispatch: concurrency: group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true permissions: contents: read env: COMPOSE_FILES: -f docker-compose.yml -f docker-compose.build.yml CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} jobs: web: name: Web Checks runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Check out repository uses: actions/checkout@v7 - name: Set up Bun uses: oven-sh/setup-bun@v2 - name: Install dependencies run: bun install --frozen-lockfile - name: Check Prettier formatting run: bun run format:check:prettier - name: Lint web app run: bun run lint:web - name: Type-check web app run: bun run typecheck:web - name: Run web unit tests with coverage run: bun run test:web:coverage - name: Normalize web coverage paths for Codecov run: | python - <<'PY' from pathlib import Path source = Path('apps/web/coverage/lcov.info') target = Path('apps/web/coverage/lcov.codecov.info') normalized_lines = [] for line in source.read_text(encoding='utf-8').splitlines(): if line.startswith('SF:'): path = line[3:].replace('\\', '/') marker = '/apps/web/' if marker in path: path = 'apps/web/' + path.split(marker, 1)[1] elif not path.startswith('apps/web/'): path = 'apps/web/' + path.lstrip('./') line = f'SF:{path}' normalized_lines.append(line) target.write_text('\n'.join(normalized_lines) + '\n', encoding='utf-8') PY - name: Preview web coverage source paths shell: bash run: | grep '^SF:' apps/web/coverage/lcov.codecov.info | head -n 20 - name: Upload web coverage artifact if: always() uses: actions/upload-artifact@v7 with: name: web-coverage path: | apps/web/coverage/lcov.info apps/web/coverage/lcov.codecov.info if-no-files-found: error - name: Upload web coverage to Codecov if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && env.CODECOV_TOKEN != '' }} uses: codecov/codecov-action@v7 with: files: apps/web/coverage/lcov.codecov.info flags: frontend disable_search: true token: ${{ env.CODECOV_TOKEN }} fail_ci_if_error: false - name: Build web app run: bun run build:web api: name: API Checks runs-on: ubuntu-latest timeout-minutes: 20 defaults: run: working-directory: apps/api steps: - name: Check out repository uses: actions/checkout@v7 - name: Set up Go uses: actions/setup-go@v7 with: go-version-file: apps/api/go.mod cache-dependency-path: apps/api/go.sum - name: Check gofmt formatting shell: bash run: | unformatted="$(gofmt -l .)" if [ -n "$unformatted" ]; then echo "The following Go files are not gofmt-formatted:" echo "$unformatted" exit 1 fi - name: Run API tests with coverage shell: bash run: go test $(go list ./... | grep -v '/internal/testutil$') -coverprofile=coverage.out -covermode=atomic - name: Print API coverage summary run: go tool cover -func=coverage.out - name: Rewrite API coverage paths for Codecov shell: bash run: | sed 's#^github.com/xdrop/monorepo/#apps/api/#' coverage.out > coverage.codecov.out - name: Upload API coverage artifact if: always() uses: actions/upload-artifact@v7 with: name: api-coverage path: | apps/api/coverage.out apps/api/coverage.codecov.out if-no-files-found: error - name: Upload API coverage to Codecov if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && env.CODECOV_TOKEN != '' }} uses: codecov/codecov-action@v7 with: files: apps/api/coverage.codecov.out flags: backend disable_search: true token: ${{ env.CODECOV_TOKEN }} fail_ci_if_error: false - name: Build API binary run: go build -o /tmp/xdrop-api ./cmd/api e2e: name: Compose Smoke And E2E runs-on: ubuntu-latest timeout-minutes: 35 needs: - web - api steps: - name: Check out repository uses: actions/checkout@v7 - name: Set up Bun uses: oven-sh/setup-bun@v2 - name: Install dependencies run: bun install --frozen-lockfile - name: Install Playwright browser run: bunx playwright install --with-deps chromium - name: Start Docker stack run: docker compose ${{ env.COMPOSE_FILES }} up -d --build - name: Wait for Xdrop endpoints shell: bash run: | for _ in {1..60}; do if curl -fsS http://localhost:8080/healthz >/dev/null && curl -fsS http://localhost:8080/ >/dev/null; then exit 0 fi sleep 5 done echo "Xdrop endpoints did not become ready in time" >&2 exit 1 - name: Run browser E2E suite run: bun run test:e2e - name: Capture Docker logs if: always() run: docker compose ${{ env.COMPOSE_FILES }} logs --no-color > compose-logs.txt - name: Upload Playwright report if: always() uses: actions/upload-artifact@v7 with: name: playwright-report path: playwright-report if-no-files-found: ignore - name: Upload Playwright test results if: always() uses: actions/upload-artifact@v7 with: name: playwright-test-results path: test-results if-no-files-found: ignore - name: Upload Docker logs if: always() uses: actions/upload-artifact@v7 with: name: compose-logs path: compose-logs.txt if-no-files-found: ignore - name: Shut down Docker stack if: always() run: docker compose ${{ env.COMPOSE_FILES }} down -v