admin/.gitea/workflows/deploy-vps.yml
alisaza a307400f83 ci: migrate Actions from GitHub to Gitea registry [skip ci]
Move workflows under .gitea/workflows and publish images to
git.ghabilee.ir instead of ghcr.io so deploy runs on self-hosted Gitea.
2026-09-13 14:49:37 +03:30

226 lines
8.8 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

name: Deploy admin to VPS
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: deploy-admin-production
cancel-in-progress: true
permissions:
contents: read
packages: write
# PR merges: build + deploy only (quality ran on pull_request).
# Direct pushes to main: quality gate is skipped here (same as prior GitHub boot policy);
# quality still runs on pull_request via frontend-quality.yml.
jobs:
gate:
name: Detect direct push to main
runs-on: ubuntu-latest
outputs:
run_quality: ${{ steps.detect.outputs.run_quality }}
steps:
- id: detect
env:
# Via env — never interpolate commit text into the script body
# (backticks/`$()` in messages would otherwise become shell command substitution).
EVENT_NAME: ${{ github.event_name }}
COMMIT_MSG: ${{ github.event.head_commit.message || '' }}
run: |
set -euo pipefail
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
echo "run_quality=false" >> "$GITHUB_OUTPUT"
exit 0
fi
msg="$COMMIT_MSG"
if printf '%s' "$msg" | grep -qiE 'merge pull request #[0-9]+'; then
echo "run_quality=false" >> "$GITHUB_OUTPUT"
elif printf '%s' "$msg" | grep -qE '\(#[0-9]+\)[[:space:]]*$'; then
echo "run_quality=false" >> "$GITHUB_OUTPUT"
else
# Admin boot: deploy first; quality runs on pull_request workflow.
echo "Direct push to main — skipping quality gate for deploy."
echo "run_quality=false" >> "$GITHUB_OUTPUT"
fi
quality:
needs: gate
if: needs.gate.outputs.run_quality == 'true'
uses: ./.gitea/workflows/frontend-quality.yml
permissions:
contents: read
pull-requests: read
build:
name: Build and push admin image
needs: [gate, quality]
if: >-
always() &&
needs.gate.result == 'success' &&
(needs.quality.result == 'success' || needs.quality.result == 'skipped')
runs-on: ubuntu-latest
timeout-minutes: 45
outputs:
image: ${{ steps.meta.outputs.image }}
steps:
- uses: actions/checkout@v4
- name: Image metadata
id: meta
run: |
set -euo pipefail
owner="$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')"
repo="$(echo '${{ github.event.repository.name }}' | tr '[:upper:]' '[:lower:]')"
# Gitea container registry (same host as git.ghabilee.ir)
echo "image=git.ghabilee.ir/${owner}/${repo}" >> "$GITHUB_OUTPUT"
echo "registry=git.ghabilee.ir" >> "$GITHUB_OUTPUT"
- name: Fetch production build environment
env:
SSH_KEY: ${{ secrets.VPS_SSH_KEY }}
VPS_HOST: ${{ secrets.VPS_HOST }}
VPS_USER: ${{ secrets.VPS_USER }}
run: |
set -euo pipefail
install -m 700 -d "$HOME/.ssh"
printf '%s\n' "$SSH_KEY" > "$HOME/.ssh/vps_key"
chmod 600 "$HOME/.ssh/vps_key"
env_path="$(ssh -i "$HOME/.ssh/vps_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new \
"${VPS_USER}@${VPS_HOST}" \
'test -s /opt/ghabilee-admin/.env && printf %s /opt/ghabilee-admin/.env')"
scp -i "$HOME/.ssh/vps_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new \
"${VPS_USER}@${VPS_HOST}:${env_path}" .env.production
test -s .env.production
rm -f "$HOME/.ssh/vps_key"
- uses: docker/setup-buildx-action@v3
- name: Log in to Gitea container registry
uses: docker/login-action@v3
with:
registry: ${{ steps.meta.outputs.registry }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
env:
ADMIN_ENV_FILE: .env.production
IMAGE_REPO: ${{ steps.meta.outputs.image }}
IMAGE_TAG: ${{ github.sha }}
run: ./scripts/ci-build-image.sh
- name: Remove production build environment
if: always()
run: rm -f .env.production
deploy:
name: Stage or deploy admin
needs: build
if: ${{ !cancelled() && needs.build.result == 'success' }}
runs-on: ubuntu-latest
timeout-minutes: 25
permissions:
contents: read
packages: read
steps:
- uses: actions/checkout@v4
- name: Copy Compose definition and deploy
env:
REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REGISTRY_USER: ${{ github.actor }}
REGISTRY_HOST: git.ghabilee.ir
ADMIN_IMAGE: ${{ needs.build.outputs.image }}:${{ github.sha }}
VPS_HOST: ${{ secrets.VPS_HOST }}
VPS_USER: ${{ secrets.VPS_USER }}
SSH_KEY: ${{ secrets.VPS_SSH_KEY }}
run: |
set -euo pipefail
install -m 700 -d "$HOME/.ssh"
printf '%s\n' "$SSH_KEY" > "$HOME/.ssh/vps_key"
chmod 600 "$HOME/.ssh/vps_key"
ssh -i "$HOME/.ssh/vps_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new \
"${VPS_USER}@${VPS_HOST}" '
set -eu
install -d -m 0750 /opt/ghabilee-admin
test -s /opt/ghabilee-admin/.env
'
scp -i "$HOME/.ssh/vps_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new \
deploy/docker-compose.production.yml \
"${VPS_USER}@${VPS_HOST}:/opt/ghabilee-admin/docker-compose.yml"
ssh -i "$HOME/.ssh/vps_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new \
"${VPS_USER}@${VPS_HOST}" \
"REGISTRY_TOKEN='${REGISTRY_TOKEN}' REGISTRY_USER='${REGISTRY_USER}' REGISTRY_HOST='${REGISTRY_HOST}' ADMIN_IMAGE='${ADMIN_IMAGE}' sh -s" <<'REMOTE'
set -eu
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" -u "$REGISTRY_USER" --password-stdin
docker pull "$ADMIN_IMAGE"
cd /opt/ghabilee-admin
ADMIN_IMAGE="$ADMIN_IMAGE" docker compose -f docker-compose.yml up -d --no-deps ghabilee-admin
for attempt in $(seq 1 36); do
health="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' ghabilee-admin 2>/dev/null || echo missing)"
echo "Admin health ${attempt}/36: ${health}"
if [ "$health" = healthy ]; then
# فقط بعد از healthy: ایمیج‌های unused (تگ‌های قبلی) را پاک کن؛ volumeها دست نخورند
if [ -x /opt/ghabilee/scripts/docker-prune.sh ]; then
/opt/ghabilee/scripts/docker-prune.sh full
else
docker image prune -af
fi
exit 0
fi
case "$health" in unhealthy|exited|dead|missing) exit 1;; esac
sleep 5
done
exit 1
REMOTE
rm -f "$HOME/.ssh/vps_key"
notify-success:
name: Notify Telegram (success)
needs: deploy
# `quality` is intentionally skipped after PR merges. `success()` treats
# that skipped upstream job as non-success and would skip this job too.
if: ${{ always() && needs.deploy.result == 'success' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Notify ops group of successful admin deploy
env:
DEPLOY_SHA: ${{ github.sha }}
DEPLOY_STATUS: success
DEPLOY_COMMIT_SUBJECT: ${{ github.event.head_commit.message }}
SSH_KEY: ${{ secrets.VPS_SSH_KEY }}
VPS_HOST: ${{ secrets.VPS_HOST }}
VPS_USER: ${{ secrets.VPS_USER }}
run: |
export DEPLOY_VERSION="$(node -p "require('./package.json').version")"
chmod +x scripts/notify-via-vps.sh scripts/notify-ops-telegram.sh scripts/notify-deploy.sh
./scripts/notify-via-vps.sh
notify-failed:
name: Notify Telegram (failed)
needs: [gate, quality, build, deploy]
if: failure()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Notify ops group of failed admin deploy
env:
DEPLOY_SHA: ${{ github.sha }}
DEPLOY_STATUS: failed
DEPLOY_COMMIT_SUBJECT: ${{ github.event.head_commit.message }}
SSH_KEY: ${{ secrets.VPS_SSH_KEY }}
VPS_HOST: ${{ secrets.VPS_HOST }}
VPS_USER: ${{ secrets.VPS_USER }}
run: |
# Do not gate on -x: notify-via-vps.sh may be 100644 in git; chmod first
# (the old `if [[ -x ... ]]` skipped the whole notify and still exited 0).
export DEPLOY_VERSION="$(node -p "require('./package.json').version" 2>/dev/null || echo '?')"
chmod +x scripts/notify-via-vps.sh scripts/notify-ops-telegram.sh scripts/notify-deploy.sh
./scripts/notify-via-vps.sh