name: CI/CD on: push: branches: [main] pull_request: branches: [main] concurrency: group: hamkadr-cicd-${{ github.ref }} cancel-in-progress: true # ───────────────────────────────────────────────────────────────────────────── # Runner labels (act_runner): # 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 via Nexus at mirror.soroushasadi.com. # Required Gitea secret: ENV_FILE (contents of .env) # ───────────────────────────────────────────────────────────────────────────── jobs: # ── CI: compile-check (every push / PR) ────────────────────────────────────── ci: name: "CI · 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 (Nexus) run: | cat > /tmp/nuget.ci.config << 'EOF' EOF - name: Restore run: dotnet restore src/JobsMedical.Web/JobsMedical.Web.csproj --configfile /tmp/nuget.ci.config env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 - name: Build run: dotnet build src/JobsMedical.Web/JobsMedical.Web.csproj --no-restore -c Release # ── CD: build image → deploy on the server (push to main only) ──────────────── deploy: name: "Deploy · hamkadr" runs-on: self-hosted env: # act host runner starts with a minimal PATH — extend so docker/snap resolve. PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin needs: [ci] if: github.event_name == 'push' && github.ref == 'refs/heads/main' timeout-minutes: 30 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: Tag current image for rollback (before rebuild) run: | if docker image inspect mirror.soroushasadi.com/hamkadr/api:latest >/dev/null 2>&1; then docker tag mirror.soroushasadi.com/hamkadr/api:latest mirror.soroushasadi.com/hamkadr/api:rollback echo "Tagged previous image → :rollback" else echo "No previous image — first deploy." fi - name: Back up database (if running) run: | set -a; . ./.env; set +a if docker ps -q --filter name='^hamkadr_db$' | grep -q .; then BACKUP_DIR="/opt/hamkadr-backups"; mkdir -p "$BACKUP_DIR" STAMP=$(date +%Y%m%d-%H%M%S) docker exec hamkadr_db pg_dump -U "${POSTGRES_USER:-hamkadr}" "${POSTGRES_DB:-hamkadr}" \ > "$BACKUP_DIR/hamkadr-$STAMP.sql" \ && echo "✅ DB backed up → $BACKUP_DIR/hamkadr-$STAMP.sql" \ || echo "⚠️ backup failed (non-fatal)" ls -t "$BACKUP_DIR"/*.sql 2>/dev/null | tail -n +11 | xargs -r rm else echo "ℹ️ hamkadr_db not running — first deploy, nothing to back up." fi - name: Build image run: docker compose build api env: DOCKER_BUILDKIT: 1 COMPOSE_DOCKER_CLI_BUILD: 1 - name: Start database run: docker compose up -d --no-deps db - name: Deploy app (stop + rm + up — reliable across docker versions) run: | docker stop hamkadr_api 2>/dev/null || true docker rm hamkadr_api 2>/dev/null || true docker compose up -d --no-deps api - name: Wait for healthy run: | echo "Waiting for hamkadr_api (up to 3 min)..." for i in $(seq 1 36); do HEALTH=$(docker inspect --format='{{.State.Health.Status}}' hamkadr_api 2>/dev/null || echo "missing") STATE=$(docker inspect --format='{{.State.Status}}' hamkadr_api 2>/dev/null || echo "missing") RESTARTS=$(docker inspect --format='{{.RestartCount}}' hamkadr_api 2>/dev/null || echo "0") echo " [$i/36] state=$STATE health=$HEALTH restarts=$RESTARTS" [ "$HEALTH" = "healthy" ] && echo "✅ hamkadr_api healthy" && exit 0 if [ "$STATE" = "exited" ] || [ "$STATE" = "dead" ]; then echo "❌ hamkadr_api crashed — logs:"; docker logs hamkadr_api 2>&1 | tail -120; exit 1 fi if [ "$RESTARTS" -gt 1 ]; then echo "❌ hamkadr_api crash-loop — logs:"; docker logs hamkadr_api 2>&1 | tail -120; exit 1 fi [ "$i" = "36" ] && echo "❌ timeout" && docker logs hamkadr_api 2>&1 | tail -80 && exit 1 sleep 5 done - name: Show containers if: always() run: docker compose ps - name: Prune old hamkadr images if: success() # Only untagged () hamkadr images — never touches other projects. run: | docker images --format '{{.Repository}}:{{.Tag}} {{.ID}}' \ | grep '^mirror\.soroushasadi\.com/hamkadr/' \ | grep '' \ | awk '{print $2}' \ | xargs -r docker rmi || true