From a8cd6fc25276bcd99baa35f4016f2adeaa2d2ec1 Mon Sep 17 00:00:00 2001 From: alisaza Date: Sun, 13 Sep 2026 18:34:21 +0330 Subject: [PATCH] ci: build admin on Finland, run on Iran, notify from Finland [skip ci] Align with ops topology: act_runner/Telegram on Finland, runtime on Iran. --- .gitea/BRANCH_PROTECTION.md | 66 ++++++------- .gitea/workflows/deploy-vps.yml | 68 +++++-------- .gitea/workflows/notify-telegram.yml | 16 ++- scripts/deploy-finland-to-iran.sh | 141 +++++++++++++++++++++++++++ scripts/notify-from-finland.sh | 16 +++ 5 files changed, 216 insertions(+), 91 deletions(-) create mode 100755 scripts/deploy-finland-to-iran.sh create mode 100755 scripts/notify-from-finland.sh diff --git a/.gitea/BRANCH_PROTECTION.md b/.gitea/BRANCH_PROTECTION.md index 3947b98..4817aaf 100644 --- a/.gitea/BRANCH_PROTECTION.md +++ b/.gitea/BRANCH_PROTECTION.md @@ -1,49 +1,41 @@ -# Branch protection on `main` (Gitea) +# Branch protection / CI (Gitea) -Configure in Gitea: +## Architecture -**Repository → Settings → Branches → Add branch protection rule → Branch name pattern: `main`** +| Role | Where | +| ---- | ----- | +| Git + Actions runner + Telegram notify | Finland (`git.ghabilee.ir`) | +| Admin runtime (`ghabilee-admin` container) | Iran VPS | -Recommended settings: +Build the image on Finland, stream it to Iran, then `docker compose up` there. -- [x] Enable push -- [x] Enable merge -- [x] Require pull request reviews (optional for solo work) -- [x] Enable status check -- [x] Require branches to be up to date before merging +## Actions secrets -### Required status checks +**Iran runtime (already set as `VPS_*`):** -- `Dependency vulnerability scan` -- `Secret scan` -- `Build, test, and quality checks` +| Secret | Meaning | +| ------ | ------- | +| `VPS_HOST` | Iran IP (e.g. `95.38.160.241`) | +| `VPS_USER` | usually `root` | +| `VPS_SSH_KEY` | Iran private key | -Until status checks are wired, use **Pull Request → merge** (not direct push) and rely on -local Husky `pre-push` (`pnpm prepush:check`). +**Finland build host (required):** -### Gitea Actions secrets +| Secret | Meaning | +| ------ | ------- | +| `FINLAND_HOST` | e.g. `65.108.18.151` | +| `FINLAND_USER` | usually `root` | +| `FINLAND_SSH_KEY` | same key telegrambot uses as `DEPLOY_SSH_KEY` | -Repo → Settings → Actions → Secrets: +**Telegram (required — same values as telegrambot repo):** -| Secret | Purpose | -| ------------- | --------------------------------------------- | -| `VPS_SSH_KEY` | Private key for SSH deploy to the Iran VPS | -| `VPS_HOST` | VPS host/IP reachable from the Actions runner | -| `VPS_USER` | SSH user (usually `root`) | +| Secret | Meaning | +| ------ | ------- | +| `TELEGRAM_BOT_TOKEN` | bot token | +| `TELEGRAM_GROUP_CHAT_ID` | ops group | +| `TELEGRAM_GROUP_THREAD_ID` | forum topic id (optional) | +| `TELEGRAM_CHAT_ID` | private fallback (optional) | -`GITHUB_TOKEN` is injected automatically by Gitea Actions (used for the container registry). +## Manual notify test -### Container registry - -Images publish to: `git.ghabilee.ir//:` - -### Telegram deploy alerts - -Notify scripts SSH into the VPS and use Telegram credentials already on the server -(`/opt/ghabilee-admin` / shared ops env). No extra Gitea secrets are required for notify -unless you change `scripts/notify-via-vps.sh`. - -### Remotes - -- Gitea (canonical): `https://git.ghabilee.ir/AliSaZa/admin.git` -- Git SSH: `ssh://git@git.ghabilee.ir:222/AliSaZa/admin.git` +Actions → **Notify Telegram (manual)** → Run workflow diff --git a/.gitea/workflows/deploy-vps.yml b/.gitea/workflows/deploy-vps.yml index b1df8d8..35df889 100644 --- a/.gitea/workflows/deploy-vps.yml +++ b/.gitea/workflows/deploy-vps.yml @@ -1,7 +1,7 @@ name: Deploy admin to VPS -# act_runner job containers have no Docker daemon. Build on the VPS (same -# pattern as telegrambot) after rsyncing sources. +# Finland (git / act_runner): build image + Telegram notify +# Iran: runtime only (compose up pre-built image) on: push: branches: [main] @@ -13,56 +13,36 @@ concurrency: jobs: deploy: - name: Rsync and deploy admin + name: Build on Finland, run on Iran runs-on: ubuntu-latest timeout-minutes: 90 steps: - uses: actions/checkout@v4 - - name: Deploy over SSH + - name: Deploy (Finland build → Iran runtime) env: - SSH_KEY: ${{ secrets.VPS_SSH_KEY }} - VPS_HOST: ${{ secrets.VPS_HOST }} - VPS_USER: ${{ secrets.VPS_USER }} + FINLAND_SSH_KEY: ${{ secrets.FINLAND_SSH_KEY }} + FINLAND_HOST: ${{ secrets.FINLAND_HOST }} + FINLAND_USER: ${{ secrets.FINLAND_USER }} + IRAN_SSH_KEY: ${{ secrets.VPS_SSH_KEY }} + IRAN_HOST: ${{ secrets.VPS_HOST }} + IRAN_USER: ${{ secrets.VPS_USER }} run: | set -euo pipefail - test -n "${SSH_KEY:-}" - test -n "${VPS_HOST:-}" - test -n "${VPS_USER:-}" + test -n "${FINLAND_SSH_KEY:-}" + test -n "${FINLAND_HOST:-}" + test -n "${FINLAND_USER:-}" + test -n "${IRAN_SSH_KEY:-}" + test -n "${IRAN_HOST:-}" + test -n "${IRAN_USER:-}" - # act_runner images are minimal; telegrambot deploy installs these too. if ! command -v rsync >/dev/null 2>&1 || ! command -v ssh >/dev/null 2>&1; then apt-get update -qq apt-get install -y -qq rsync openssh-client fi - install -m 700 -d "$HOME/.ssh" - printf '%s\n' "$SSH_KEY" > "$HOME/.ssh/deploy_key" - chmod 600 "$HOME/.ssh/deploy_key" - - SSH=(ssh -i "$HOME/.ssh/deploy_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new) - - "${SSH[@]}" "${VPS_USER}@${VPS_HOST}" ' - set -eu - install -d -m 0750 /opt/ghabilee-admin /opt/ghabilee-admin/src - test -s /opt/ghabilee-admin/.env - ' - - rsync -az --delete \ - -e "ssh -i $HOME/.ssh/deploy_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" \ - --exclude '.git' \ - --exclude 'node_modules' \ - --exclude '.next' \ - --exclude '.env' \ - --exclude '.env.*' \ - --exclude 'test-results' \ - --exclude 'playwright-report' \ - ./ "${VPS_USER}@${VPS_HOST}:/opt/ghabilee-admin/src/" - - "${SSH[@]}" "${VPS_USER}@${VPS_HOST}" \ - 'chmod +x /opt/ghabilee-admin/src/scripts/deploy-on-vps.sh && APP_DIR=/opt/ghabilee-admin SRC_DIR=/opt/ghabilee-admin/src /opt/ghabilee-admin/src/scripts/deploy-on-vps.sh' - - rm -f "$HOME/.ssh/deploy_key" + chmod +x scripts/deploy-finland-to-iran.sh + IMAGE_TAG="ghabilee-admin:${GITHUB_SHA}" ./scripts/deploy-finland-to-iran.sh - name: Notify Telegram if: always() @@ -71,15 +51,15 @@ jobs: DEPLOY_SHA: ${{ github.sha }} DEPLOY_STATUS: ${{ job.status }} DEPLOY_COMMIT_SUBJECT: ${{ github.event.head_commit.message }} - SSH_KEY: ${{ secrets.VPS_SSH_KEY }} - VPS_HOST: ${{ secrets.VPS_HOST }} - VPS_USER: ${{ secrets.VPS_USER }} + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} + TELEGRAM_GROUP_CHAT_ID: ${{ secrets.TELEGRAM_GROUP_CHAT_ID }} + TELEGRAM_GROUP_THREAD_ID: ${{ secrets.TELEGRAM_GROUP_THREAD_ID }} + TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} run: | export DEPLOY_VERSION="$(node -p "require('./package.json').version" 2>/dev/null || echo '?')" case "${DEPLOY_STATUS}" in success) export DEPLOY_STATUS=success ;; *) export DEPLOY_STATUS=failed ;; esac - chmod +x scripts/notify-via-vps.sh scripts/notify-ops-telegram.sh scripts/notify-deploy.sh - # Creds from Iran VPS .env; send from this foreign runner (Telegram blocked in Iran). - ./scripts/notify-via-vps.sh + chmod +x scripts/notify-from-finland.sh scripts/notify-ops-telegram.sh scripts/notify-deploy.sh + ./scripts/notify-from-finland.sh diff --git a/.gitea/workflows/notify-telegram.yml b/.gitea/workflows/notify-telegram.yml index 88f4341..1aedab8 100644 --- a/.gitea/workflows/notify-telegram.yml +++ b/.gitea/workflows/notify-telegram.yml @@ -1,6 +1,5 @@ name: Notify Telegram (manual) -# Lightweight check that deploy alerts work without rebuilding the admin image. on: workflow_dispatch: @@ -17,15 +16,12 @@ jobs: DEPLOY_SHA: ${{ github.sha }} DEPLOY_STATUS: success DEPLOY_COMMIT_SUBJECT: manual notify test - SSH_KEY: ${{ secrets.VPS_SSH_KEY }} - VPS_HOST: ${{ secrets.VPS_HOST }} - VPS_USER: ${{ secrets.VPS_USER }} + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} + TELEGRAM_GROUP_CHAT_ID: ${{ secrets.TELEGRAM_GROUP_CHAT_ID }} + TELEGRAM_GROUP_THREAD_ID: ${{ secrets.TELEGRAM_GROUP_THREAD_ID }} + TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} run: | set -euo pipefail - if ! command -v ssh >/dev/null 2>&1 || ! command -v scp >/dev/null 2>&1; then - apt-get update -qq - apt-get install -y -qq openssh-client - fi 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 + chmod +x scripts/notify-from-finland.sh scripts/notify-ops-telegram.sh scripts/notify-deploy.sh + ./scripts/notify-from-finland.sh diff --git a/scripts/deploy-finland-to-iran.sh b/scripts/deploy-finland-to-iran.sh new file mode 100755 index 0000000..c410589 --- /dev/null +++ b/scripts/deploy-finland-to-iran.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +# Build admin image on the Finland host, then load+run it on Iran. +# +# Architecture: +# - Finland (git.ghabilee.ir / act_runner): build + Telegram notify +# - Iran: runtime only (/opt/ghabilee-admin) +set -euo pipefail + +: "${FINLAND_SSH_KEY:?}" +: "${FINLAND_HOST:?}" +: "${FINLAND_USER:?}" +: "${IRAN_SSH_KEY:?}" +: "${IRAN_HOST:?}" +: "${IRAN_USER:?}" + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +IMAGE_TAG="${IMAGE_TAG:-ghabilee-admin:$(git rev-parse --short HEAD 2>/dev/null || date +%s)}" +FINLAND_APP="/opt/ghabilee-admin-ci" +IRAN_APP="/opt/ghabilee-admin" + +install -m 700 -d "$HOME/.ssh" +printf '%s\n' "$FINLAND_SSH_KEY" > "$HOME/.ssh/finland_key" +printf '%s\n' "$IRAN_SSH_KEY" > "$HOME/.ssh/iran_key" +chmod 600 "$HOME/.ssh/finland_key" "$HOME/.ssh/iran_key" +trap 'rm -f "$HOME/.ssh/finland_key" "$HOME/.ssh/iran_key"' EXIT + +FSSH=(ssh -i "$HOME/.ssh/finland_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new) +ISSH=(ssh -i "$HOME/.ssh/iran_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new) +FRSYNC=(-e "ssh -i $HOME/.ssh/finland_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new") + +# --- build env from Iran (runtime .env holds NEXT_PUBLIC_* bake inputs) --- +"${ISSH[@]}" "${IRAN_USER}@${IRAN_HOST}" "test -s ${IRAN_APP}/.env" +scp -i "$HOME/.ssh/iran_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new \ + "${IRAN_USER}@${IRAN_HOST}:${IRAN_APP}/.env" .env.production +test -s .env.production + +# --- sync sources + env to Finland build dir --- +"${FSSH[@]}" "${FINLAND_USER}@${FINLAND_HOST}" "install -d -m 0750 '${FINLAND_APP}/src'" +rsync -az --delete "${FRSYNC[@]}" \ + --exclude '.git' \ + --exclude 'node_modules' \ + --exclude '.next' \ + --exclude '.env' \ + --exclude '.env.*' \ + --exclude 'test-results' \ + --exclude 'playwright-report' \ + ./ "${FINLAND_USER}@${FINLAND_HOST}:${FINLAND_APP}/src/" + +scp -i "$HOME/.ssh/finland_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new \ + .env.production "${FINLAND_USER}@${FINLAND_HOST}:${FINLAND_APP}/.env" +rm -f .env.production + +# --- build on Finland (enough RAM; Telegram/git live here too) --- +"${FSSH[@]}" "${FINLAND_USER}@${FINLAND_HOST}" \ + "IMAGE_TAG='${IMAGE_TAG}' APP_DIR='${FINLAND_APP}' SRC_DIR='${FINLAND_APP}/src' bash -s" <<'REMOTE' +set -euo pipefail +cd "$APP_DIR" +# noglob: backend-style cron values must never expand if present in .env +set -f +# shellcheck disable=SC1091 +set -a +# shellcheck disable=SC1090 +source "$APP_DIR/.env" +set +a +set +f + +env_or_empty() { printf '%s' "${!1-}"; } + +sanitize_api_proxy_target() { + local target + target="$(env_or_empty API_PROXY_TARGET)" + case "$target" in + *127.0.0.1*|*localhost*|*'::1'*) printf '' ;; + *) printf '%s' "$target" ;; + esac +} + +# Avoid glob expansion when sourcing cron-like values already done; build args only. +docker build --platform linux/amd64 -t "$IMAGE_TAG" \ + --build-arg "NEXT_PUBLIC_API_URL=$(env_or_empty NEXT_PUBLIC_API_URL)" \ + --build-arg "NEXT_PUBLIC_FILE_SERVER_URL=$(env_or_empty NEXT_PUBLIC_FILE_SERVER_URL)" \ + --build-arg "MAP_API_KEY=$(env_or_empty MAP_API_KEY)" \ + --build-arg "NEXT_PUBLIC_MAP_API_KEY=$(env_or_empty NEXT_PUBLIC_MAP_API_KEY)" \ + --build-arg "NEXT_PUBLIC_VAPID_PUBLIC_KEY=$(env_or_empty NEXT_PUBLIC_VAPID_PUBLIC_KEY)" \ + --build-arg "NEXT_PUBLIC_BASE_PATH=$(env_or_empty NEXT_PUBLIC_BASE_PATH)" \ + --build-arg "NEXT_PUBLIC_SITE_URL=$(env_or_empty NEXT_PUBLIC_SITE_URL)" \ + --build-arg "NEXT_PUBLIC_OBSERVABILITY_ENDPOINT=$(env_or_empty NEXT_PUBLIC_OBSERVABILITY_ENDPOINT)" \ + --build-arg "NEXT_PUBLIC_SENTRY_DSN=$(env_or_empty NEXT_PUBLIC_SENTRY_DSN)" \ + --build-arg "NEXT_PUBLIC_SENTRY_ENVIRONMENT=$(env_or_empty NEXT_PUBLIC_SENTRY_ENVIRONMENT)" \ + --build-arg "NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=$(env_or_empty NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE)" \ + --build-arg "NEXT_PUBLIC_ARCAPTCHA_SITE_KEY=$(env_or_empty NEXT_PUBLIC_ARCAPTCHA_SITE_KEY)" \ + --build-arg "SENTRY_AUTH_TOKEN=$(env_or_empty SENTRY_AUTH_TOKEN)" \ + --build-arg "SENTRY_ORG=$(env_or_empty SENTRY_ORG)" \ + --build-arg "SENTRY_PROJECT=$(env_or_empty SENTRY_PROJECT)" \ + --build-arg "API_PROXY_TARGET=$(sanitize_api_proxy_target)" \ + "$SRC_DIR" + +docker image prune -af >/dev/null 2>&1 || true +echo "BUILT $IMAGE_TAG" +REMOTE + +# --- stream image Finland → Iran, then compose up --- +"${ISSH[@]}" "${IRAN_USER}@${IRAN_HOST}" "install -d -m 0750 '${IRAN_APP}'" +scp -i "$HOME/.ssh/iran_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new \ + deploy/docker-compose.production.yml \ + "${IRAN_USER}@${IRAN_HOST}:${IRAN_APP}/docker-compose.yml" +if [[ -f deploy/nginx/backoffice.conf ]]; then + scp -i "$HOME/.ssh/iran_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new \ + deploy/nginx/backoffice.conf \ + "${IRAN_USER}@${IRAN_HOST}:/etc/nginx/sites-available/backoffice" + "${ISSH[@]}" "${IRAN_USER}@${IRAN_HOST}" ' + ln -sfn /etc/nginx/sites-available/backoffice /etc/nginx/sites-enabled/backoffice + nginx -t && systemctl reload nginx + ' +fi + +echo "Transferring ${IMAGE_TAG} Finland → Iran..." +"${FSSH[@]}" "${FINLAND_USER}@${FINLAND_HOST}" "docker save '${IMAGE_TAG}'" \ + | "${ISSH[@]}" "${IRAN_USER}@${IRAN_HOST}" "docker load" + +"${ISSH[@]}" "${IRAN_USER}@${IRAN_HOST}" \ + "ADMIN_IMAGE='${IMAGE_TAG}' bash -s" <<'REMOTE' +set -euo pipefail +cd /opt/ghabilee-admin +test -s .env +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 + docker image prune -af >/dev/null 2>&1 || true + echo "Deploy OK" + exit 0 + fi + case "$health" in unhealthy|exited|dead|missing) exit 1 ;; esac + sleep 5 +done +exit 1 +REMOTE diff --git a/scripts/notify-from-finland.sh b/scripts/notify-from-finland.sh new file mode 100755 index 0000000..41bd5e1 --- /dev/null +++ b/scripts/notify-from-finland.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Notify ops Telegram from the Finland Actions runner (not from Iran). +# Credentials come from Gitea Actions secrets — same model as telegrambot. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +: "${TELEGRAM_BOT_TOKEN:?TELEGRAM_BOT_TOKEN secret required}" + +if [[ -z "${TELEGRAM_GROUP_CHAT_ID:-}${TELEGRAM_CHAT_ID:-}" ]]; then + echo "[ghabilee-admin-notify] failed: set TELEGRAM_GROUP_CHAT_ID or TELEGRAM_CHAT_ID" >&2 + exit 1 +fi + +chmod +x "${SCRIPT_DIR}/notify-ops-telegram.sh" "${SCRIPT_DIR}/notify-deploy.sh" +bash "${SCRIPT_DIR}/notify-deploy.sh"