name: CI/CD # Pushes to Gitea trigger this. GitHub (origin) stays a backup and does not deploy. on: push: branches: [master] pull_request: branches: [master] concurrency: group: flatrender-cicd-${{ github.ref }} cancel-in-progress: true jobs: # ── CI: fast frontend type-check before the (long) deploy build ─────────────── web-check: name: "CI · Web (tsc)" 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 deps run: | npm ci --no-audit --no-fund \ --registry https://mirror.soroushasadi.com/repository/npm-group/ \ --fetch-retries=5 --fetch-retry-maxtimeout=120000 - name: TypeScript check run: npx tsc --noEmit # ── Deploy: build + bring up the whole compose stack on the server ──────────── deploy: name: "Deploy · full stack" runs-on: self-hosted needs: [web-check] if: github.event_name == 'push' && github.ref == 'refs/heads/master' timeout-minutes: 50 env: # act runner host mode ships a minimal PATH — extend so docker/snap resolve. PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin # Lock the compose project name so volumes keep a STABLE prefix across deploys # regardless of the runner's checkout directory (prevents orphaned-volume data loss). COMPOSE_PROJECT_NAME: flatrender COMPOSE_FILE: docker-compose.v2.yml DOCKER_BUILDKIT: "1" COMPOSE_DOCKER_CLI_BUILD: "1" 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 -f FETCH_HEAD - name: Write .env (from ENV_FILE secret) run: printf '%s' "$ENV_FILE" > .env env: ENV_FILE: ${{ secrets.ENV_FILE }} - name: Backup database (if running) run: | if docker ps -a --format '{{.Names}}' | grep -q '^fr2-postgres$'; then mkdir -p /opt/flatrender-backups set -a; . ./.env 2>/dev/null || true; set +a STAMP=$(date +%Y%m%d-%H%M%S) echo "Dumping DB → /opt/flatrender-backups/flatrender-$STAMP.sql" docker exec fr2-postgres pg_dump -U "${POSTGRES_USER:-postgres}" flatrender \ > "/opt/flatrender-backups/flatrender-$STAMP.sql" || echo "backup failed (non-fatal)" # keep only the 14 most recent dumps ls -1t /opt/flatrender-backups/flatrender-*.sql 2>/dev/null | tail -n +15 | xargs -r rm -f else echo "fr2-postgres not running yet — first deploy, no backup needed." fi - name: Build images run: docker compose build --parallel - name: Start stack run: docker compose up -d --remove-orphans - name: Wait for gateway healthy run: | for i in $(seq 1 30); do S=$(docker inspect --format='{{.State.Health.Status}}' fr2-gateway 2>/dev/null || echo missing) echo " [$i/30] gateway: $S" [ "$S" = "healthy" ] && echo "OK gateway healthy" && break [ "$i" = "30" ] && echo "TIMEOUT gateway" && docker compose logs --tail=60 gateway content-svc identity-svc && exit 1 sleep 6 done - name: Wait for frontend healthy run: | for i in $(seq 1 24); do S=$(docker inspect --format='{{.State.Health.Status}}' fr2-frontend 2>/dev/null || echo missing) echo " [$i/24] frontend: $S" [ "$S" = "healthy" ] && echo "OK frontend healthy" && break [ "$i" = "24" ] && echo "TIMEOUT frontend" && docker compose logs --tail=60 frontend && exit 1 sleep 5 done - name: Prune dangling images if: success() run: docker image prune -f