Align CI/CD with soroush method (DrSousan single-app pattern)
CI/CD / CI · dotnet build (push) Successful in 2m58s
CI/CD / Deploy · hamkadr (push) Failing after 6m23s

Audited against working Meezi/DrSousan pipelines. Fixes:
- Single docker-compose.yml is the production stack (api + internal db); folded in docker-compose.prod.yml; dev Postgres → docker-compose.dev.yml
- Dockerfile HEALTHCHECK (bash /dev/tcp) so deploy's docker-inspect Health.Status wait works
- Naming to convention: service api, container hamkadr_api/hamkadr_db, image mirror.soroushasadi.com/hamkadr/api:${API_TAG}
- Workflow rewritten to DrSousan pattern: ci build + deploy (rollback-tag before build, pg_dump backup, stop/rm/up, docker-inspect health-wait with crash detection, scoped image prune)
- environment: block with ${VAR:-default} substitution (no hard-failing env_file); HOST_PORT; .env excluded from image context
- nginx vhost + DEPLOY.md updated to match

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-03 23:38:22 +03:30
parent 36bb165438
commit 8f5d926d42
8 changed files with 189 additions and 134 deletions
+80 -44
View File
@@ -1,17 +1,28 @@
name: CI/CD
on:
push: { branches: [main] }
pull_request: { branches: [main] }
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
build:
name: "CI — dotnet build (Release)"
# ── CI: compile-check (every push / PR) ──────────────────────────────────────
ci:
name: "CI · dotnet build"
runs-on: ubuntu-latest
container:
image: mirror.soroushasadi.com/dotnet/sdk:10.0
@@ -44,23 +55,23 @@ jobs:
- name: Restore
run: dotnet restore src/JobsMedical.Web/JobsMedical.Web.csproj --configfile /tmp/nuget.ci.config
env: { DOTNET_CLI_TELEMETRY_OPTOUT: 1 }
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
- name: Build
run: dotnet build src/JobsMedical.Web/JobsMedical.Web.csproj --no-restore -c Release
# ---------------------------------------------------------------- Deploy
# ── CD: build image → deploy on the server (push to main only) ────────────────
deploy:
name: "Deploy hamkadr (compose)"
name: "Deploy · hamkadr"
runs-on: self-hosted
needs: [build]
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/snap resolve.
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
DOCKER_BUILDKIT: 1
COMPOSE_DOCKER_CLI_BUILD: 1
needs: [ci]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
timeout-minutes: 30
steps:
- name: Checkout
env:
@@ -75,52 +86,77 @@ jobs:
- name: Write .env
run: printf '%s' "$ENV_FILE" > .env
env: { ENV_FILE: ${{ secrets.ENV_FILE }} }
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 -a --format '{{.Names}}' | grep -q '^hamkadr-db$'; then
mkdir -p /opt/hamkadr-backups
TS=$(date +%Y%m%d-%H%M%S)
echo "Backing up DB → /opt/hamkadr-backups/hamkadr-$TS.sql"
docker exec hamkadr-db pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" \
> "/opt/hamkadr-backups/hamkadr-$TS.sql" || echo "WARN: backup skipped (db not ready yet)"
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 "No existing db container — first deploy, nothing to back up."
echo "️ hamkadr_db not running — first deploy, nothing to back up."
fi
- name: Tag current image for rollback
run: |
if docker image inspect hamkadr-app:latest >/dev/null 2>&1; then
docker tag hamkadr-app:latest hamkadr-app:rollback
echo "Tagged hamkadr-app:rollback"
fi
- name: Build app image
run: docker compose -f docker-compose.prod.yml build app
- name: Build image
run: docker compose build api
env:
DOCKER_BUILDKIT: 1
COMPOSE_DOCKER_CLI_BUILD: 1
- name: Start database
run: docker compose -f docker-compose.prod.yml up -d --no-deps db
run: docker compose up -d --no-deps db
- name: Recreate app (stop + rm + up — reliable across docker versions)
- name: Deploy app (stop + rm + up — reliable across docker versions)
run: |
docker stop hamkadr-app 2>/dev/null || true
docker rm hamkadr-app 2>/dev/null || true
docker compose -f docker-compose.prod.yml up -d --no-deps app
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 app healthy
- name: Wait for healthy
run: |
set -a; . ./.env; set +a
for i in $(seq 1 24); do
if curl -fsS "http://127.0.0.1:${APP_PORT}/healthz" >/dev/null 2>&1; then
echo "OK — hamkadr-app healthy on 127.0.0.1:${APP_PORT}"; exit 0
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
echo " [$i/24] not ready yet…"
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
echo "TIMEOUT — dumping logs"; docker logs --tail=60 hamkadr-app; exit 1
- name: Prune dangling images
- name: Show containers
if: always()
run: docker compose ps
- name: Prune old hamkadr images
if: success()
run: docker image prune -f
# Only untagged (<none>) hamkadr images — never touches other projects.
run: |
docker images --format '{{.Repository}}:{{.Tag}} {{.ID}}' \
| grep '^mirror\.soroushasadi\.com/hamkadr/' \
| grep '<none>' \
| awk '{print $2}' \
| xargs -r docker rmi || true