Move workflows under .gitea/workflows and publish images to git.ghabilee.ir instead of ghcr.io so deploy runs on self-hosted Gitea.
226 lines
8.8 KiB
YAML
226 lines
8.8 KiB
YAML
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
|