#!/usr/bin/env bash # deploy.sh — deploy release to EC2 # Works in two modes: # 1. Automated (after compile.sh): uses dist/ built in the same job # 2. Manual/rollback: downloads assets from Forgejo Release by tag set -euo pipefail TAG="$1" echo "==> deploy: tag=${TAG}" # Download from Forgejo if dist/ was not built in this job if [ ! -f dist/jiu-server ] || [ ! -f dist/web.tar.gz ] || [ ! -f dist/configs.tar.gz ] || [ ! -f dist/marketing.tar.gz ]; then echo "==> deploy: dist/ incomplete — downloading assets for ${TAG} from Forgejo" mkdir -p dist curl -kf \ -H "Authorization: token ${FORGEJO_TOKEN}" \ "${FORGEJO_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/tags/${TAG}" \ -o /tmp/release-info.json python3 - <<'PYEOF' import json, urllib.request, ssl, os, sys ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE with open('/tmp/release-info.json') as f: release = json.load(f) token = os.environ['FORGEJO_TOKEN'] forgejo_url = os.environ['FORGEJO_URL'] repo = os.environ['GITEA_REPOSITORY'] release_id = release['id'] for asset in release.get('assets', []): url = f"{forgejo_url}/api/v1/repos/{repo}/releases/{release_id}/assets/{asset['id']}" req = urllib.request.Request(url, headers={'Authorization': f'token {token}'}) with urllib.request.urlopen(req, context=ctx) as resp: with open(f"dist/{asset['name']}", 'wb') as out: out.write(resp.read()) print(f"Downloaded: {asset['name']}") PYEOF fi echo "==> deploy: dist/ contents:" ls -lh dist/ # Extract configs (nginx, version.yaml, etc.) rm -rf /tmp/jiu-configs mkdir -p /tmp/jiu-configs tar -xzf dist/configs.tar.gz -C /tmp/jiu-configs # Extract Flutter web build rm -rf /tmp/jiu-web-new mkdir -p /tmp/jiu-web-new tar -xzf dist/web.tar.gz -C /tmp/jiu-web-new --strip-components=1 # Extract marketing site rm -rf /tmp/jiu-marketing-new mkdir -p /tmp/jiu-marketing-new tar -xzf dist/marketing.tar.gz -C /tmp/jiu-marketing-new # Setup SSH mkdir -p ~/.ssh printf '%s' "${EC2_SSH_KEY}" > ~/.ssh/ec2_deploy.pem chmod 600 ~/.ssh/ec2_deploy.pem ssh-keyscan -H "${EC2_HOST}" >> ~/.ssh/known_hosts SSH="ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no" SCP="scp -O -i ~/.ssh/ec2_deploy.pem" echo "==> deploy: uploading files to EC2" # Upload backend binary ${SCP} dist/jiu-server "${EC2_USER}@${EC2_HOST}:/tmp/jiu-server" # Upload version.yaml ${SCP} /tmp/jiu-configs/backend/config/version.yaml "${EC2_USER}@${EC2_HOST}:/tmp/version.yaml" # Upload nginx config ${SCP} /tmp/jiu-configs/deploy/nginx-jiu.conf "${EC2_USER}@${EC2_HOST}:/tmp/nginx-jiu.conf" # Upload Flutter web rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no" \ /tmp/jiu-web-new/ "${EC2_USER}@${EC2_HOST}:/tmp/jiu-web-new/" # Upload marketing site (built by compile.sh) rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no" \ /tmp/jiu-marketing-new/ "${EC2_USER}@${EC2_HOST}:/tmp/jiu-marketing-new/" # Upload desktop client installers (served from /downloads/ by nginx). # Guarded: may be absent in a partial manual deploy. [ -f dist/jiu-windows-x64-setup.exe ] && ${SCP} dist/jiu-windows-x64-setup.exe "${EC2_USER}@${EC2_HOST}:/tmp/jiu-windows-x64-setup.exe" || true [ -f dist/jiu-macos-x64.zip ] && ${SCP} dist/jiu-macos-x64.zip "${EC2_USER}@${EC2_HOST}:/tmp/jiu-macos-x64.zip" || true [ -f dist/jiu-android.apk ] && ${SCP} dist/jiu-android.apk "${EC2_USER}@${EC2_HOST}:/tmp/jiu-android.apk" || true echo "==> deploy: running remote deploy" ${SSH} "${EC2_USER}@${EC2_HOST}" << 'ENDSSH' set -e # Replace backend binary sudo systemctl stop jiu cp /tmp/jiu-server /opt/jiu/backend/jiu-server chmod +x /opt/jiu/backend/jiu-server # Update version config(WorkingDirectory=/opt/jiu,读 config/version.yaml) mkdir -p /opt/jiu/config cp /tmp/version.yaml /opt/jiu/config/version.yaml # Start and health check sudo systemctl start jiu echo "Waiting for health check..." for i in $(seq 1 30); do if curl -f http://localhost:8080/health > /dev/null 2>&1; then echo "Health check passed" break fi sleep 2 done curl -f http://localhost:8080/health > /dev/null || { echo "Health check failed!"; exit 1; } # Swap Flutter web app (atomic) rm -rf /opt/jiu/web-old mv /opt/jiu/web /opt/jiu/web-old 2>/dev/null || true mv /tmp/jiu-web-new /opt/jiu/web # Update marketing site mkdir -p /opt/jiu/marketing rsync -a --delete /tmp/jiu-marketing-new/ /opt/jiu/marketing/ rm -rf /tmp/jiu-marketing-new # Publish desktop client installers to the nginx-served downloads dir. # Keep only the latest version: clear old installers, then drop in the new ones. mkdir -p /opt/jiu/downloads rm -f /opt/jiu/downloads/jiu-windows-* /opt/jiu/downloads/jiu-macos-* /opt/jiu/downloads/jiu-android-* /opt/jiu/downloads/jiu-android.apk 2>/dev/null || true [ -f /tmp/jiu-windows-x64-setup.exe ] && mv -f /tmp/jiu-windows-x64-setup.exe /opt/jiu/downloads/ || true [ -f /tmp/jiu-macos-x64.zip ] && mv -f /tmp/jiu-macos-x64.zip /opt/jiu/downloads/ || true [ -f /tmp/jiu-android.apk ] && mv -f /tmp/jiu-android.apk /opt/jiu/downloads/ || true # Update jiu nginx config in the pangolin-edge reverse proxy. # nginx now runs inside the `pangolin-edge` container (host networking), not on # the host. Its /etc/nginx/conf.d is bind-mounted from the host dir below, so we # write the config there and reload nginx *inside the container*. cp /tmp/nginx-jiu.conf /home/ec2-user/pangolin/edge/conf.d/jiu.conf docker exec pangolin-edge nginx -t && docker exec pangolin-edge nginx -s reload echo "Deploy complete!" ENDSSH # Cleanup SSH key rm -f ~/.ssh/ec2_deploy.pem rm -rf /tmp/jiu-configs /tmp/jiu-web-new echo "==> deploy: done"