name: CI/CD on: push: branches: [main] pull_request: branches: [main] concurrency: group: meezi-cicd-${{ github.ref }} cancel-in-progress: true # ───────────────────────────────────────────────────────────────────────────── # HOW THIS WORKS # ───────────────────────────────────────────────────────────────────────────── # Runner labels (in gitea docker-compose): # ubuntu-latest:docker://node:20-alpine ← CI jobs run in real Docker containers # self-hosted:host ← deploy runs directly on the server # # All images/packages served from Nexus at mirror.soroushasadi.com: # Docker images → mirror.soroushasadi.com (docker-group: Docker Hub + MCR) # NuGet → https://mirror.soroushasadi.com/repository/nuget-group/ # npm → https://mirror.soroushasadi.com/repository/npm-group/ # # Docker daemon: merge docker/daemon-registry-mirror.example.json into daemon.json # ───────────────────────────────────────────────────────────────────────────── jobs: api-build: name: "CI · API (dotnet build + test)" runs-on: ubuntu-latest container: image: mirror.soroushasadi.com/dotnet/sdk:10.0 options: >- --add-host=gitea:host-gateway services: postgres: image: mirror.soroushasadi.com/postgres:16-alpine env: POSTGRES_DB: meezi_test POSTGRES_USER: meezi POSTGRES_PASSWORD: meezi_test_pass options: >- --health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 10 redis: image: mirror.soroushasadi.com/redis:7-alpine options: >- --health-cmd "redis-cli ping" --health-interval 5s --health-timeout 3s --health-retries 10 steps: - name: Checkout env: TOKEN: ${{ github.token }} REF: ${{ github.ref }} run: | git init git remote add origin "${{ github.server_url }}/${{ github.repository }}.git" git config http.extraheader "Authorization: Bearer ${TOKEN}" git fetch --depth=1 origin "${REF}" git checkout FETCH_HEAD - name: Write NuGet config run: | cat > /tmp/nuget.ci.config << 'EOF' EOF - name: Restore run: dotnet restore src/Meezi.API/Meezi.API.csproj --configfile /tmp/nuget.ci.config env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 - name: Build run: dotnet build src/Meezi.API/Meezi.API.csproj --no-restore -c Release - name: Test run: dotnet test --no-build -c Release --logger "console;verbosity=minimal" env: ConnectionStrings__DefaultConnection: "Host=postgres;Port=5432;Database=meezi_test;Username=meezi;Password=meezi_test_pass" ConnectionStrings__Redis: "redis:6379" admin-api-build: name: "CI · Admin API (dotnet build)" runs-on: ubuntu-latest container: image: mirror.soroushasadi.com/dotnet/sdk:10.0 options: >- --add-host=gitea:host-gateway steps: - name: Checkout env: TOKEN: ${{ github.token }} REF: ${{ github.ref }} run: | git init git remote add origin "${{ github.server_url }}/${{ github.repository }}.git" git config http.extraheader "Authorization: Bearer ${TOKEN}" git fetch --depth=1 origin "${REF}" git checkout FETCH_HEAD - name: Write NuGet config run: | cat > /tmp/nuget.ci.config << 'EOF' EOF - name: Restore run: dotnet restore src/Meezi.Admin.API/Meezi.Admin.API.csproj --configfile /tmp/nuget.ci.config env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 - name: Build run: dotnet build src/Meezi.Admin.API/Meezi.Admin.API.csproj --no-restore -c Release dashboard-check: name: "CI · Dashboard (tsc)" runs-on: ubuntu-latest container: image: mirror.soroushasadi.com/node:20-alpine options: >- --add-host=gitea:host-gateway steps: - name: Checkout env: TOKEN: ${{ github.token }} SHA: ${{ github.sha }} run: | wget -q \ --header "Authorization: Bearer ${TOKEN}" \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/archive/${SHA}.tar.gz" \ -O /tmp/repo.tar.gz tar -xzf /tmp/repo.tar.gz --strip-components=1 rm -f /tmp/repo.tar.gz - name: Install dependencies working-directory: web/dashboard run: npm install --legacy-peer-deps --ignore-scripts --registry https://mirror.soroushasadi.com/repository/npm-group/ --strict-ssl=false - name: TypeScript check working-directory: web/dashboard run: npx tsc --noEmit env: NEXT_PUBLIC_API_URL: http://localhost:5080 admin-web-check: name: "CI · Admin Web (tsc)" runs-on: ubuntu-latest container: image: mirror.soroushasadi.com/node:20-alpine options: >- --add-host=gitea:host-gateway steps: - name: Checkout env: TOKEN: ${{ github.token }} SHA: ${{ github.sha }} run: | wget -q \ --header "Authorization: Bearer ${TOKEN}" \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/archive/${SHA}.tar.gz" \ -O /tmp/repo.tar.gz tar -xzf /tmp/repo.tar.gz --strip-components=1 rm -f /tmp/repo.tar.gz - name: Install dependencies working-directory: web/admin run: npm install --legacy-peer-deps --ignore-scripts --registry https://mirror.soroushasadi.com/repository/npm-group/ --strict-ssl=false - name: TypeScript check working-directory: web/admin run: npx tsc --noEmit env: NEXT_PUBLIC_ADMIN_API_URL: http://localhost:5081 website-check: name: "CI · Website (tsc)" runs-on: ubuntu-latest container: image: mirror.soroushasadi.com/node:20-alpine options: >- --add-host=gitea:host-gateway steps: - name: Checkout env: TOKEN: ${{ github.token }} SHA: ${{ github.sha }} run: | wget -q \ --header "Authorization: Bearer ${TOKEN}" \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/archive/${SHA}.tar.gz" \ -O /tmp/repo.tar.gz tar -xzf /tmp/repo.tar.gz --strip-components=1 rm -f /tmp/repo.tar.gz - name: Install dependencies working-directory: web/website run: npm install --legacy-peer-deps --ignore-scripts --registry https://mirror.soroushasadi.com/repository/npm-group/ --strict-ssl=false - name: TypeScript check working-directory: web/website run: npx tsc --noEmit env: MEEZI_API_URL: http://localhost:5080 koja-check: name: "CI · Koja (tsc)" runs-on: ubuntu-latest container: image: mirror.soroushasadi.com/node:20-alpine options: >- --add-host=gitea:host-gateway steps: - name: Checkout env: TOKEN: ${{ github.token }} SHA: ${{ github.sha }} run: | wget -q \ --header "Authorization: Bearer ${TOKEN}" \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/archive/${SHA}.tar.gz" \ -O /tmp/repo.tar.gz tar -xzf /tmp/repo.tar.gz --strip-components=1 rm -f /tmp/repo.tar.gz - name: Install dependencies working-directory: web/koja run: npm install --legacy-peer-deps --ignore-scripts --registry https://mirror.soroushasadi.com/repository/npm-group/ --strict-ssl=false - name: TypeScript check working-directory: web/koja run: npx tsc --noEmit env: NEXT_PUBLIC_API_URL: http://localhost:5080 # ───────────────────────────────────────────────────────────────────────────── # DEPLOY — only on push to main, only if ALL CI jobs pass. # self-hosted:host — runs directly on your server where Docker is installed. # ───────────────────────────────────────────────────────────────────────────── deploy: name: "Deploy · all services" runs-on: self-hosted env: # act runner (host mode) starts with a minimal PATH — extend it so # docker (/usr/bin or /usr/local/bin) and snap packages are found. PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin needs: - api-build - admin-api-build - dashboard-check - admin-web-check - website-check - koja-check if: github.event_name == 'push' && github.ref == 'refs/heads/main' timeout-minutes: 40 steps: - name: Checkout env: TOKEN: ${{ github.token }} REF: ${{ github.ref }} run: | git init git remote add origin "${{ github.server_url }}/${{ github.repository }}.git" git config http.extraheader "Authorization: Bearer ${TOKEN}" git fetch --depth=1 origin "${REF}" git checkout FETCH_HEAD - name: Write .env run: printf '%s' "$ENV_FILE" > .env env: ENV_FILE: ${{ secrets.ENV_FILE }} - name: Build main images (api, web, website, koja) run: docker compose build --parallel api web website koja env: DOCKER_BUILDKIT: 1 COMPOSE_DOCKER_CLI_BUILD: 1 - name: Build admin images (admin-api, admin-web) run: | docker compose \ -f docker-compose.yml \ -f docker-compose.admin.yml \ build --parallel admin-api admin-web env: DOCKER_BUILDKIT: 1 COMPOSE_DOCKER_CLI_BUILD: 1 - name: Deploy main app services # Only cycle the app containers — postgres and redis are long-running # infrastructure that must never be restarted by CI. # --force-recreate: always swap in the freshly-built images. # --no-deps: do NOT pull in postgres/redis as dependencies. run: | docker compose up -d \ --no-deps \ --force-recreate \ api web website koja - name: Deploy admin services run: | docker compose \ -f docker-compose.yml \ -f docker-compose.admin.yml \ up -d \ --no-deps \ --force-recreate \ admin-api admin-web - name: Wait for main API healthy run: | for i in $(seq 1 24); do STATUS=$(docker inspect --format='{{.State.Health.Status}}' meezi-api 2>/dev/null || echo "missing") echo " [$i/24] $STATUS" [ "$STATUS" = "healthy" ] && echo "✅ meezi-api healthy" && break [ "$i" = "24" ] && echo "❌ meezi-api timeout" && docker compose logs --tail=40 api && exit 1 sleep 5 done - name: Wait for admin API healthy run: | for i in $(seq 1 24); do STATUS=$(docker inspect --format='{{.State.Health.Status}}' meezi-admin-api 2>/dev/null || echo "missing") echo " [$i/24] $STATUS" [ "$STATUS" = "healthy" ] && echo "✅ meezi-admin-api healthy" && break [ "$i" = "24" ] && echo "❌ meezi-admin-api timeout" && docker compose -f docker-compose.yml -f docker-compose.admin.yml logs --tail=40 admin-api && exit 1 sleep 5 done - name: Show all running containers if: always() run: docker compose -f docker-compose.yml -f docker-compose.admin.yml ps # Intentionally no image pruning — disk cleanup is done manually on the server.