name: CI/CD on: push: branches: [main] pull_request: branches: [main] # Only one deploy at a time; a newer push cancels an in-progress one 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 # # WHY we don't use actions/checkout@v4 in container jobs: # actions/checkout is a JS action — needs `node` in the container. # mcr.microsoft.com/dotnet/sdk → no Node.js → exit 127 # node:20-alpine → no git → checkout fails # Fix: plain shell git clone via http.extraheader (token never in process list). # # Local mirrors — Nexus Repository Manager (docker-compose.mirror.yml): # "mirror" hostname → host-gateway (docker bridge IP 172.17.0.1) # NuGet → http://mirror:8081/repository/nuget-proxy/ (nuget.mirror.config) # npm → http://mirror:8081/repository/npm-proxy/ (--registry flag) # Docker → http://mirror:8083 (daemon.json registry-mirrors) # ───────────────────────────────────────────────────────────────────────────── jobs: # ── Main API ──────────────────────────────────────────────────────────────── api-build: name: "CI · API (dotnet build + test)" runs-on: ubuntu-latest container: image: mcr.microsoft.com/dotnet/sdk:10.0 options: >- --add-host=gitea:host-gateway --add-host=mirror:host-gateway services: postgres: image: 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: 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: Restore run: dotnet restore src/Meezi.API/Meezi.API.csproj --source http://mirror:8081/repository/nuget-group/index.json - 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 ─────────────────────────────────────────────────────────────── admin-api-build: name: "CI · Admin API (dotnet build)" runs-on: ubuntu-latest container: image: mcr.microsoft.com/dotnet/sdk:10.0 options: >- --add-host=gitea:host-gateway --add-host=mirror: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: Restore run: dotnet restore src/Meezi.Admin.API/Meezi.Admin.API.csproj --source http://mirror:8081/repository/nuget-group/index.json - name: Build run: dotnet build src/Meezi.Admin.API/Meezi.Admin.API.csproj --no-restore -c Release # ── Dashboard ─────────────────────────────────────────────────────────────── dashboard-check: name: "CI · Dashboard (tsc)" runs-on: ubuntu-latest container: image: node:20-alpine options: >- --add-host=gitea:host-gateway --add-host=mirror:host-gateway steps: - name: Checkout env: TOKEN: ${{ github.token }} REF: ${{ github.ref }} run: | apk add --no-cache git 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: Install dependencies working-directory: web/dashboard run: npm install --legacy-peer-deps --ignore-scripts --registry http://mirror:8081/repository/npm-proxy/ - name: TypeScript check working-directory: web/dashboard run: npx tsc --noEmit env: NEXT_PUBLIC_API_URL: http://localhost:5080 # ── Admin Web ─────────────────────────────────────────────────────────────── admin-web-check: name: "CI · Admin Web (tsc)" runs-on: ubuntu-latest container: image: node:20-alpine options: >- --add-host=gitea:host-gateway --add-host=mirror:host-gateway steps: - name: Checkout env: TOKEN: ${{ github.token }} REF: ${{ github.ref }} run: | apk add --no-cache git 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: Install dependencies working-directory: web/admin run: npm install --legacy-peer-deps --ignore-scripts --registry http://mirror:8081/repository/npm-proxy/ - name: TypeScript check working-directory: web/admin run: npx tsc --noEmit env: NEXT_PUBLIC_ADMIN_API_URL: http://localhost:5081 # ── Website ───────────────────────────────────────────────────────────────── website-check: name: "CI · Website (tsc)" runs-on: ubuntu-latest container: image: node:20-alpine options: >- --add-host=gitea:host-gateway --add-host=mirror:host-gateway steps: - name: Checkout env: TOKEN: ${{ github.token }} REF: ${{ github.ref }} run: | apk add --no-cache git 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: Install dependencies working-directory: web/website run: npm install --legacy-peer-deps --ignore-scripts --registry http://mirror:8081/repository/npm-proxy/ - name: TypeScript check working-directory: web/website run: npx tsc --noEmit env: MEEZI_API_URL: http://localhost:5080 # ── Finder ────────────────────────────────────────────────────────────────── finder-check: name: "CI · Finder (tsc)" runs-on: ubuntu-latest container: image: node:20-alpine options: >- --add-host=gitea:host-gateway --add-host=mirror:host-gateway steps: - name: Checkout env: TOKEN: ${{ github.token }} REF: ${{ github.ref }} run: | apk add --no-cache git 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: Install dependencies working-directory: web/finder run: npm install --legacy-peer-deps --ignore-scripts --registry http://mirror:8081/repository/npm-proxy/ - name: TypeScript check working-directory: web/finder 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. # The runner itself (gitea/act_runner) has node+git, so actions/checkout works. # ───────────────────────────────────────────────────────────────────────────── deploy: name: "Deploy · all services" runs-on: self-hosted needs: - api-build - admin-api-build - dashboard-check - admin-web-check - website-check - finder-check if: github.event_name == 'push' && github.ref == 'refs/heads/main' timeout-minutes: 40 steps: - uses: actions/checkout@v4 - name: Write .env run: printf '%s' "$ENV_FILE" > .env env: ENV_FILE: ${{ secrets.ENV_FILE }} - name: Build main images (api, web, website, finder) run: docker compose build --parallel api web website finder 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: Start main services run: | docker compose up -d \ --remove-orphans \ --no-deps \ postgres redis api web website finder - name: Start admin services run: | docker compose \ -f docker-compose.yml \ -f docker-compose.admin.yml \ up -d \ --no-deps \ 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 - name: Prune old images if: success() run: docker image prune -f