name: CI on: pull_request: push: branches: [master] workflow_dispatch: concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: check: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install uv run: python -m pip install --upgrade pip uv - name: Cache uv uses: actions/cache@v4 with: path: | ~/.cache/uv backend/.venv key: uv-${{ runner.os }}-${{ hashFiles('backend/uv.lock') }} - name: Set up Node id: setup-node uses: actions/setup-node@v4 with: node-version: "22" cache: npm cache-dependency-path: frontend/package-lock.json - name: Install backend dependencies run: make backend-sync - name: Install frontend dependencies run: make frontend-sync - name: Cache Next.js build cache uses: actions/cache@v4 with: path: | frontend/.next/cache key: nextjs-${{ runner.os }}-node-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('frontend/package-lock.json') }} restore-keys: | nextjs-${{ runner.os }}-node-${{ steps.setup-node.outputs.node-version }}- - name: Enforce one migration per PR if: ${{ github.event_name == 'pull_request' }} env: GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }} run: | ./scripts/ci/one_migration_per_pr.sh - name: Run migration integrity gate if: ${{ github.event_name == 'pull_request' }} run: | set -euo pipefail if [ "${{ github.event_name }}" = "pull_request" ]; then BASE_SHA="${{ github.event.pull_request.base.sha }}" HEAD_SHA="${{ github.sha }}" git fetch --no-tags --depth=1 origin "$BASE_SHA" else BASE_SHA="${{ github.event.before }}" HEAD_SHA="${{ github.sha }}" fi CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA") echo "Changed files:" echo "$CHANGED_FILES" if ! echo "$CHANGED_FILES" | grep -Eq '^backend/(app/models|db|migrations|alembic\.ini)'; then echo "No migration-relevant backend changes detected; skipping migration gate." exit 0 fi if echo "$CHANGED_FILES" | grep -Eq '^backend/app/models/' && ! echo "$CHANGED_FILES" | grep -Eq '^backend/migrations/versions/'; then echo "Model changes detected without a migration under backend/migrations/versions/." exit 1 fi make backend-migration-check - name: Run backend checks env: # Keep CI builds deterministic. AUTH_MODE: "local" LOCAL_AUTH_TOKEN: "ci-local-auth-token-0123456789-0123456789-0123456789x" run: | make backend-lint make backend-coverage - name: Run frontend checks env: # Keep CI builds deterministic. NEXT_TELEMETRY_DISABLED: "1" NEXT_PUBLIC_API_URL: "http://localhost:8000" NEXT_PUBLIC_AUTH_MODE: "local" run: | make frontend-lint make frontend-typecheck make frontend-test make frontend-build - name: Docs quality gates run: | make docs-check - name: Upload coverage artifacts if: always() uses: actions/upload-artifact@v4 with: name: coverage if-no-files-found: ignore path: | backend/coverage.xml frontend/coverage/** installer: name: Installer (${{ matrix.os }}) runs-on: ${{ matrix.os }} needs: [check] strategy: fail-fast: false matrix: include: - os: ubuntu-latest run_linux_smoke_tests: true run_macos_local_smoke_test: false - os: macos-latest run_linux_smoke_tests: false run_macos_local_smoke_test: true steps: - name: Checkout uses: actions/checkout@v4 - name: Validate installer shell syntax run: bash -n install.sh - name: Set up Python for macOS installer smoke test if: ${{ matrix.run_macos_local_smoke_test }} uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install uv for macOS installer smoke test if: ${{ matrix.run_macos_local_smoke_test }} run: python -m pip install --upgrade pip uv - name: Set up Node for macOS installer smoke test if: ${{ matrix.run_macos_local_smoke_test }} uses: actions/setup-node@v4 with: node-version: "22" - name: Start PostgreSQL for macOS installer smoke test if: ${{ matrix.run_macos_local_smoke_test }} run: | brew install postgresql@16 PG_BIN="$(brew --prefix postgresql@16)/bin" "$PG_BIN/initdb" -D "$RUNNER_TEMP/pgdata" "$PG_BIN/pg_ctl" -D "$RUNNER_TEMP/pgdata" -l "$RUNNER_TEMP/postgres.log" -o "-p 55432" start "$PG_BIN/createdb" -p 55432 mission_control - name: Installer smoke test (macOS local mode + external db) if: ${{ matrix.run_macos_local_smoke_test }} run: | PGUSER="$(whoami)" ./install.sh \ --mode local \ --backend-port 18002 \ --frontend-port 13002 \ --public-host localhost \ --api-url http://localhost:18002 \ --token-mode generate \ --db-mode external \ --database-url "postgresql+psycopg://${PGUSER}@localhost:55432/mission_control" \ --start-services no test -f .env test -f backend/.env test -f frontend/.env test -f frontend/.next/BUILD_ID - name: Installer smoke test (docker mode) if: ${{ matrix.run_linux_smoke_tests }} run: | ./install.sh \ --mode docker \ --backend-port 18000 \ --frontend-port 13000 \ --public-host localhost \ --api-url http://localhost:18000 \ --token-mode generate backend_ready=0 for _ in {1..120}; do if curl -fsS http://127.0.0.1:18000/healthz >/dev/null; then backend_ready=1 break fi sleep 2 done frontend_ready=0 for _ in {1..120}; do if curl -fsS http://127.0.0.1:13000 >/dev/null; then frontend_ready=1 break fi sleep 2 done if [ "$backend_ready" -ne 1 ] || [ "$frontend_ready" -ne 1 ]; then echo "Installer docker smoke readiness failed: backend_ready=$backend_ready frontend_ready=$frontend_ready" docker compose -f compose.yml --env-file .env ps || true docker compose -f compose.yml --env-file .env logs --no-color --tail=200 backend db redis frontend webhook-worker || true exit 1 fi - name: Cleanup docker stack after docker mode if: ${{ always() && matrix.run_linux_smoke_tests }} run: | docker compose -f compose.yml --env-file .env down -v --remove-orphans || true - name: Installer smoke test (local mode) if: ${{ matrix.run_linux_smoke_tests }} env: XDG_STATE_HOME: ${{ github.workspace }}/.installer-state run: | ./install.sh \ --mode local \ --backend-port 18001 \ --frontend-port 13001 \ --public-host localhost \ --api-url http://localhost:18001 \ --token-mode generate \ --db-mode docker \ --start-services yes backend_ready=0 for _ in {1..120}; do if curl -fsS http://127.0.0.1:18001/healthz >/dev/null; then backend_ready=1 break fi sleep 2 done frontend_ready=0 for _ in {1..120}; do if curl -fsS http://127.0.0.1:13001 >/dev/null; then frontend_ready=1 break fi sleep 2 done if [ "$backend_ready" -ne 1 ] || [ "$frontend_ready" -ne 1 ]; then echo "Installer local smoke readiness failed: backend_ready=$backend_ready frontend_ready=$frontend_ready" LOG_DIR="$XDG_STATE_HOME/openclaw-mission-control-install" if [ -f "$LOG_DIR/backend.log" ]; then echo "----- backend log (tail) -----" tail -n 200 "$LOG_DIR/backend.log" || true fi if [ -f "$LOG_DIR/frontend.log" ]; then echo "----- frontend log (tail) -----" tail -n 200 "$LOG_DIR/frontend.log" || true fi exit 1 fi - name: Cleanup local processes and docker resources if: ${{ always() && matrix.run_linux_smoke_tests }} env: XDG_STATE_HOME: ${{ github.workspace }}/.installer-state run: | LOG_DIR="$XDG_STATE_HOME/openclaw-mission-control-install" if [ -f "$LOG_DIR/backend.pid" ]; then kill "$(cat "$LOG_DIR/backend.pid")" || true; fi if [ -f "$LOG_DIR/frontend.pid" ]; then kill "$(cat "$LOG_DIR/frontend.pid")" || true; fi docker compose -f compose.yml --env-file .env down -v --remove-orphans || true - name: Cleanup macOS PostgreSQL if: ${{ always() && matrix.run_macos_local_smoke_test }} run: | if ! command -v brew >/dev/null 2>&1; then exit 0 fi PG_PREFIX="$(brew --prefix postgresql@16 2>/dev/null || true)" if [ -z "$PG_PREFIX" ] || [ ! -d "$PG_PREFIX" ]; then exit 0 fi PG_BIN="$PG_PREFIX/bin" if [ -d "$RUNNER_TEMP/pgdata" ] && [ -x "$PG_BIN/pg_ctl" ]; then "$PG_BIN/pg_ctl" -D "$RUNNER_TEMP/pgdata" -m fast stop || true fi e2e: runs-on: ubuntu-latest needs: [check] steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Node id: setup-node uses: actions/setup-node@v4 with: node-version: "22" cache: npm cache-dependency-path: frontend/package-lock.json - name: Install frontend dependencies run: make frontend-sync - name: Cache Next.js build cache uses: actions/cache@v4 with: path: | frontend/.next/cache key: nextjs-${{ runner.os }}-node-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('frontend/package-lock.json') }} restore-keys: | nextjs-${{ runner.os }}-node-${{ steps.setup-node.outputs.node-version }}- - name: Start frontend (dev server) env: NEXT_PUBLIC_API_URL: "http://localhost:8000" NEXT_PUBLIC_AUTH_MODE: "local" NEXT_TELEMETRY_DISABLED: "1" run: | cd frontend npm run dev -- --hostname 0.0.0.0 --port 3000 & for i in {1..60}; do if curl -sf http://localhost:3000/ > /dev/null; then exit 0; fi sleep 2 done echo "Frontend did not start" exit 1 - name: Run Cypress E2E env: NEXT_PUBLIC_API_URL: "http://localhost:8000" NEXT_PUBLIC_AUTH_MODE: "local" NEXT_TELEMETRY_DISABLED: "1" run: | cd frontend npm run e2e -- --browser chrome - name: Upload Cypress artifacts if: failure() uses: actions/upload-artifact@v4 with: name: cypress-artifacts if-no-files-found: ignore path: | frontend/cypress/screenshots/** frontend/cypress/videos/**