69a630d185
NuGet loads the service index of EVERY listed source, so a 500 from the Liara fallback aborted the whole restore (NU1301) even though Nexus was healthy. Mirror cert chain is fixed now, so use our Nexus mirror as the single source of truth. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
172 lines
7.2 KiB
YAML
172 lines
7.2 KiB
YAML
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 only)
|
||
# Single source = our Nexus mirror. We do NOT list Liara as a fallback: NuGet loads
|
||
# the service index of EVERY configured source, so a 500 from a fallback aborts the
|
||
# whole restore (NU1301). Nexus is the source of truth.
|
||
run: |
|
||
cat > /tmp/nuget.ci.config << 'EOF'
|
||
<?xml version="1.0" encoding="utf-8"?>
|
||
<configuration>
|
||
<packageSources>
|
||
<clear />
|
||
<add key="nexus"
|
||
value="https://mirror.soroushasadi.com/repository/nuget-group/index.json"
|
||
protocolVersion="3" />
|
||
</packageSources>
|
||
<config>
|
||
<add key="http_retry_count" value="6" />
|
||
<add key="http_retry_delay_milliseconds" value="1000" />
|
||
</config>
|
||
</configuration>
|
||
EOF
|
||
|
||
- name: Restore
|
||
# NuGetAudit=false: the audit pings api.nuget.org for CVE data, which is
|
||
# filtered in Iran (100s timeout + NU1900 noise). The mirror has the packages.
|
||
run: dotnet restore src/JobsMedical.Web/JobsMedical.Web.csproj --configfile /tmp/nuget.ci.config -p:NuGetAudit=false
|
||
env:
|
||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||
|
||
- name: Build
|
||
run: dotnet build src/JobsMedical.Web/JobsMedical.Web.csproj --no-restore -c Release -p:NuGetAudit=false
|
||
|
||
# ── 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 (<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
|