Skip to content

Merge pull request #40 from AgoraIO-Community/feat/design-update #71

Merge pull request #40 from AgoraIO-Community/feat/design-update

Merge pull request #40 from AgoraIO-Community/feat/design-update #71

Workflow file for this run

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"