Merge pull request #40 from AgoraIO-Community/feat/design-update #71
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy Newbro VPS | |
| on: | |
| workflow_dispatch: | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - ".github/workflows/deploy-vps.yml" | |
| - "src/**" | |
| - "tests/**" | |
| - "docs/**" | |
| - "pyproject.toml" | |
| - "install.sh" | |
| - "newbro" | |
| - "package*.json" | |
| permissions: | |
| contents: read | |
| packages: write | |
| concurrency: | |
| group: newbro-vps-production | |
| cancel-in-progress: false | |
| jobs: | |
| deploy: | |
| runs-on: ubuntu-latest | |
| env: | |
| IMAGE_NAME: ghcr.io/agoraio-community/newbro | |
| DEPLOY_HOST: ${{ secrets.NEWBRO_DEPLOY_HOST }} | |
| DEPLOY_USER: ${{ secrets.NEWBRO_DEPLOY_USER }} | |
| DEPLOY_PATH: ${{ secrets.NEWBRO_DEPLOY_PATH }} | |
| DEPLOY_SSH_KEY: ${{ secrets.NEWBRO_DEPLOY_SSH_KEY }} | |
| DEPLOY_PORT: ${{ secrets.NEWBRO_DEPLOY_PORT || '22' }} | |
| PUBLIC_BASE_URL: ${{ secrets.NEWBRO_PUBLIC_BASE_URL }} | |
| SIGNUP_INVITE_CODE: ${{ secrets.NEWBRO_SIGNUP_INVITE_CODE }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "24" | |
| - name: Validate deploy config | |
| run: | | |
| test -n "$DEPLOY_HOST" | |
| test -n "$DEPLOY_USER" | |
| test -n "$DEPLOY_PATH" | |
| test -n "$DEPLOY_SSH_KEY" | |
| test -n "$PUBLIC_BASE_URL" | |
| test -n "$SIGNUP_INVITE_CODE" | |
| case "$PUBLIC_BASE_URL" in | |
| https://*) ;; | |
| *) | |
| echo "NEWBRO_PUBLIC_BASE_URL must be an https:// URL" | |
| exit 1 | |
| ;; | |
| esac | |
| - name: Install backend dependencies | |
| run: | | |
| python -m venv .venv | |
| .venv/bin/python -m pip install --upgrade pip | |
| .venv/bin/python -m pip install -e '.[dev]' | |
| - name: Run backend tests | |
| run: .venv/bin/python -m pytest | |
| - name: Build and push image | |
| env: | |
| GHCR_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euxo pipefail | |
| printf '%s' "$GHCR_TOKEN" | docker login ghcr.io -u '${{ github.actor }}' --password-stdin | |
| docker build -t "${IMAGE_NAME}:${GITHUB_SHA}" -t "${IMAGE_NAME}:main" . | |
| docker push "${IMAGE_NAME}:${GITHUB_SHA}" | |
| docker push "${IMAGE_NAME}:main" | |
| - name: Configure SSH | |
| run: | | |
| set -euxo pipefail | |
| install -m 700 -d ~/.ssh | |
| test -n "$DEPLOY_SSH_KEY" | |
| printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/newbro_deploy | |
| chmod 600 ~/.ssh/newbro_deploy | |
| ssh-keyscan -v -T 20 -p "$DEPLOY_PORT" "$DEPLOY_HOST" >> ~/.ssh/known_hosts | |
| ssh -i ~/.ssh/newbro_deploy -p "$DEPLOY_PORT" -o BatchMode=yes -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" "echo ssh-ok" | |
| - name: Deploy container on VPS | |
| env: | |
| GHCR_TOKEN: ${{ github.token }} | |
| IMAGE_REF: ${{ env.IMAGE_NAME }}:${{ github.sha }} | |
| run: | | |
| set -euxo pipefail | |
| PUBLIC_HOST="$(python - <<'PY' | |
| import os | |
| from urllib.parse import urlparse | |
| parsed = urlparse(os.environ["PUBLIC_BASE_URL"]) | |
| if parsed.scheme != "https" or not parsed.hostname: | |
| raise SystemExit("NEWBRO_PUBLIC_BASE_URL must be an https:// URL with a host") | |
| print(parsed.hostname) | |
| PY | |
| )" | |
| SIGNUP_INVITE_CODE_YAML="$(python - <<'PY' | |
| import json | |
| import os | |
| print(json.dumps(os.environ["SIGNUP_INVITE_CODE"])) | |
| PY | |
| )" | |
| ssh -i ~/.ssh/newbro_deploy -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" "mkdir -p '$DEPLOY_PATH'" | |
| ssh -i ~/.ssh/newbro_deploy -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" ' | |
| set -euxo pipefail | |
| SUDO="" | |
| if [ "$(id -u)" -ne 0 ]; then | |
| SUDO="sudo" | |
| fi | |
| if ! command -v docker >/dev/null 2>&1; then | |
| $SUDO apt-get update | |
| DEBIAN_FRONTEND=noninteractive $SUDO apt-get install -y ca-certificates curl docker.io | |
| fi | |
| if ! docker compose version >/dev/null 2>&1; then | |
| $SUDO apt-get update | |
| DEBIAN_FRONTEND=noninteractive $SUDO apt-get install -y ca-certificates curl | |
| DEBIAN_FRONTEND=noninteractive $SUDO apt-get install -y docker-compose-v2 || \ | |
| DEBIAN_FRONTEND=noninteractive $SUDO apt-get install -y docker-compose-plugin || true | |
| fi | |
| if ! docker compose version >/dev/null 2>&1; then | |
| ARCH="$(uname -m)" | |
| case "$ARCH" in | |
| x86_64|amd64) COMPOSE_ARCH="x86_64" ;; | |
| aarch64|arm64) COMPOSE_ARCH="aarch64" ;; | |
| *) echo "Unsupported Docker Compose architecture: $ARCH" >&2; exit 1 ;; | |
| esac | |
| $SUDO mkdir -p /usr/local/lib/docker/cli-plugins | |
| $SUDO curl -fsSL \ | |
| "https://github.com/docker/compose/releases/download/v2.40.3/docker-compose-linux-$COMPOSE_ARCH" \ | |
| -o /usr/local/lib/docker/cli-plugins/docker-compose | |
| $SUDO chmod +x /usr/local/lib/docker/cli-plugins/docker-compose | |
| fi | |
| $SUDO systemctl enable --now docker || true | |
| docker --version | |
| docker compose version | |
| $SUDO mkdir -p /root/.newbro | |
| ' | |
| ssh -i ~/.ssh/newbro_deploy -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" "cat > '$DEPLOY_PATH/Caddyfile'" <<EOF | |
| $PUBLIC_HOST { | |
| encode gzip | |
| reverse_proxy newbro:8000 | |
| } | |
| EOF | |
| ssh -i ~/.ssh/newbro_deploy -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" "cat > '$DEPLOY_PATH/docker-compose.yml'" <<EOF | |
| services: | |
| caddy: | |
| image: caddy:2-alpine | |
| restart: unless-stopped | |
| depends_on: | |
| - newbro | |
| ports: | |
| - "80:80" | |
| - "443:443" | |
| volumes: | |
| - ./Caddyfile:/etc/caddy/Caddyfile:ro | |
| - caddy_data:/data | |
| - caddy_config:/config | |
| newbro: | |
| image: $IMAGE_REF | |
| restart: unless-stopped | |
| ports: | |
| - "8000:8000" | |
| env_file: | |
| - /root/.newbro/.env | |
| environment: | |
| HOME: /root | |
| SYNAPSE_PUBLIC_COOKIE_SECURE: "true" | |
| NEWBRO_SIGNUP_INVITE_CODE: $SIGNUP_INVITE_CODE_YAML | |
| volumes: | |
| - /root/.newbro:/root/.newbro | |
| healthcheck: | |
| test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/health', timeout=3).read()"] | |
| interval: 30s | |
| timeout: 5s | |
| retries: 3 | |
| start_period: 20s | |
| volumes: | |
| caddy_data: | |
| caddy_config: | |
| EOF | |
| printf '%s' "$GHCR_TOKEN" | ssh -i ~/.ssh/newbro_deploy -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" "docker login ghcr.io -u '${{ github.actor }}' --password-stdin" | |
| ssh -i ~/.ssh/newbro_deploy -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" " | |
| set -euxo pipefail | |
| cd '$DEPLOY_PATH' | |
| docker container prune -f | |
| docker image prune -a -f | |
| docker compose pull | |
| docker compose up -d | |
| docker image prune -a -f | |
| docker compose ps | |
| docker system df | |
| " | |
| - name: Verify public health | |
| run: | | |
| set -euo pipefail | |
| curl --fail --show-error --silent --retry 12 --retry-delay 5 "$PUBLIC_BASE_URL/api/health" | |
| - name: Verify container health on failure | |
| if: failure() | |
| run: | | |
| ssh -i ~/.ssh/newbro_deploy -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" \ | |
| "cd '$DEPLOY_PATH' && docker compose ps && docker compose logs --tail=200 caddy && docker compose logs --tail=200 newbro && curl -i --max-time 5 http://127.0.0.1/api/health && curl -k -i --max-time 5 https://127.0.0.1/api/health && docker compose exec -T newbro python -c \"import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:8000/api/health', timeout=3).read().decode())\"" || true | |
| - name: Show container logs on failure | |
| if: failure() | |
| run: | | |
| ssh -i ~/.ssh/newbro_deploy -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" \ | |
| "cd '$DEPLOY_PATH' && docker compose ps && docker compose logs --tail=300 caddy && docker compose logs --tail=300 newbro || true" |