name: CI/CD on: push: branches: [main] pull_request: branches: [main] concurrency: group: hokm-cicd-${{ github.ref }} cancel-in-progress: true jobs: # ---------------------------------------------------------------- API (.NET) api-build: name: "CI - API (dotnet build + engine sim)" 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 -q 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 -q FETCH_HEAD - name: Write NuGet config run: | cat > /tmp/nuget.ci.config << 'EOF' EOF - name: Restore run: dotnet restore server/Hokm.slnx --configfile /tmp/nuget.ci.config env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 DOTNET_NOLOGO: 1 - name: Build run: dotnet build server/Hokm.slnx --no-restore -c Release - name: Engine simulation (rules validation) run: dotnet run --project server/tools/Hokm.Sim/Hokm.Sim.csproj -c Release --no-build # ----------------------------------------------------------- Web (Next.js) web-check: name: "CI - Web (tsc + next build)" runs-on: ubuntu-latest container: image: mirror.soroushasadi.com/node:20-alpine options: --add-host=gitea:host-gateway steps: - name: Checkout (tarball) 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 - name: Install run: npm ci --legacy-peer-deps --strict-ssl=false --registry https://mirror.soroushasadi.com/repository/npm-group/ - name: TypeScript check run: npx tsc --noEmit - name: Build (static export) run: npm run build # -------------------------------------------------------------- Deploy deploy: name: "Deploy - local stack (db + server + web)" runs-on: self-hosted needs: [api-build, web-check] if: github.event_name == 'push' && github.ref == 'refs/heads/main' timeout-minutes: 40 env: # act host runner starts with a minimal PATH — extend so docker is found. PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin steps: - name: Checkout env: TOKEN: ${{ github.token }} REF: ${{ github.ref }} run: | git init -q 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 -q FETCH_HEAD - name: Write .env run: printf '%s' "$ENV_FILE" > .env env: ENV_FILE: ${{ secrets.ENV_FILE }} - name: Backup database (if running) run: | mkdir -p /opt/hokm-backups if docker ps --format '{{.Names}}' | grep -q '^hokm-db$'; then TS=$(date +%Y%m%d-%H%M%S) docker exec hokm-db pg_dump -U hokm hokm > "/opt/hokm-backups/hokm-${TS}.sql" \ && echo "backed up to /opt/hokm-backups/hokm-${TS}.sql" \ || echo "WARN: pg_dump failed (continuing)" else echo "no hokm-db container yet — first deploy, nothing to back up" fi - name: Tag rollback image run: | CURRENT=$(docker inspect hokm-server --format='{{.Config.Image}}' 2>/dev/null || echo "") if [ -n "$CURRENT" ]; then docker tag "$CURRENT" hokm-server:rollback && echo "rollback tag = $CURRENT"; fi - name: Build images run: docker compose build --parallel server web site env: DOCKER_BUILDKIT: 1 COMPOSE_DOCKER_CLI_BUILD: 1 - name: Start database run: docker compose up -d --no-deps db - name: Wait for database healthy run: | for i in $(seq 1 20); do S=$(docker inspect --format='{{.State.Health.Status}}' hokm-db 2>/dev/null || echo missing) echo " [$i/20] db: $S" [ "$S" = "healthy" ] && break [ "$i" = "20" ] && { echo "TIMEOUT db"; docker logs --tail=40 hokm-db; exit 1; } sleep 3 done - name: Deploy server (stop + rm + up, no force-recreate) run: | docker stop hokm-server 2>/dev/null || true docker rm hokm-server 2>/dev/null || true docker compose up -d --no-deps server - name: Wait for server healthy run: | for i in $(seq 1 24); do S=$(docker inspect --format='{{.State.Health.Status}}' hokm-server 2>/dev/null || echo missing) echo " [$i/24] server: $S" [ "$S" = "healthy" ] && { echo "OK hokm-server healthy"; break; } [ "$i" = "24" ] && { echo "TIMEOUT hokm-server"; docker compose logs --tail=60 server; exit 1; } sleep 5 done - name: Deploy web (stop + rm + up, no force-recreate) run: | docker stop hokm-web 2>/dev/null || true docker rm hokm-web 2>/dev/null || true docker compose up -d --no-deps web - name: Wait for web healthy run: | for i in $(seq 1 18); do S=$(docker inspect --format='{{.State.Health.Status}}' hokm-web 2>/dev/null || echo missing) echo " [$i/18] web: $S" [ "$S" = "healthy" ] && { echo "OK hokm-web healthy"; break; } [ "$i" = "18" ] && { echo "TIMEOUT hokm-web"; docker compose logs --tail=40 web; exit 1; } sleep 5 done - name: Deploy marketing site (stop + rm + up, no force-recreate) run: | docker stop hokm-site 2>/dev/null || true docker rm hokm-site 2>/dev/null || true docker compose up -d --no-deps site - name: Wait for site healthy run: | for i in $(seq 1 18); do S=$(docker inspect --format='{{.State.Health.Status}}' hokm-site 2>/dev/null || echo missing) echo " [$i/18] site: $S" [ "$S" = "healthy" ] && { echo "OK hokm-site healthy"; break; } [ "$i" = "18" ] && { echo "TIMEOUT hokm-site"; docker compose logs --tail=40 site; exit 1; } sleep 5 done - name: Prune dangling images if: success() run: docker image prune -f