feat(deploy): improve environment variable sourcing and validation in… #88
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 to E | |
| # ============================================================================= | |
| # 所需 Secrets 清单(请在 GitHub 仓库 Settings → Secrets and variables → Actions 中配置) | |
| # ============================================================================= | |
| # 1. DATABASE_URL_E — 数据库连接地址,格式如 mysql://user:password@host:3306/dbname | |
| # 2. AUTH_SECRET — 认证加密密钥,用于 session / JWT 签名,建议 32+ 位随机字符串 | |
| # 3. DEPLOY_HOST_E — 目标服务器 IP 或域名(如 192.168.1.100 或 test.example.com) | |
| # 4. DEPLOY_USER_E — SSH 登录用户名(需对该服务器 /var/www 和 /opt/thingsvis 有写权限) | |
| # 5. DEPLOY_SSH_KEY_E — SSH 私钥(建议为部署专用用户生成独立 key pair,仅允许 key 登录) | |
| # ============================================================================= | |
| on: | |
| push: | |
| branches: [master] | |
| workflow_dispatch: | |
| env: | |
| NODE_VERSION: '20' | |
| PNPM_VERSION: '9' | |
| jobs: | |
| build-and-deploy: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v2 | |
| with: | |
| version: ${{ env.PNPM_VERSION }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'pnpm' | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| # ======================================== | |
| # Build all packages via turbo | |
| # ======================================== | |
| # DATABASE_URL_E: 数据库连接地址(如 mysql://user:pass@host:3306/thingsvis) | |
| - name: Generate Prisma Client | |
| working-directory: apps/server | |
| env: | |
| DATABASE_URL_E: ${{ secrets.DATABASE_URL_E }} | |
| run: npx prisma generate | |
| # DATABASE_URL_E: 数据库连接地址 | |
| # AUTH_SECRET: 认证加密密钥(用于 session/token 签名,建议 32+ 字符随机字符串) | |
| - name: Build Server + Studio | |
| env: | |
| DATABASE_URL_E: ${{ secrets.DATABASE_URL_E }} | |
| AUTH_SECRET: ${{ secrets.AUTH_SECRET }} | |
| run: pnpm turbo run build --filter=@thingsvis/server --filter=studio | |
| - name: Build Widgets | |
| run: pnpm run build:widgets | |
| - name: Ensure registry.json in Studio dist | |
| run: node scripts/generate-registry.js && cp apps/studio/public/registry.json apps/studio/dist/registry.json | |
| - name: Validate widget release artifacts | |
| run: node scripts/validate-widget-release.js apps/studio/dist/registry.json | |
| # ======================================== | |
| # Package artifacts | |
| # ======================================== | |
| - name: Package artifacts | |
| run: | | |
| mkdir -p deploy/server deploy/studio deploy/widgets | |
| # --- Server: Next.js standalone --- | |
| if [ -d "apps/server/.next/standalone" ]; then | |
| cp -r apps/server/.next/standalone/. deploy/server/ | |
| mkdir -p deploy/server/apps/server/.next/static | |
| cp -r apps/server/.next/static/. deploy/server/apps/server/.next/static/ | |
| if [ -d "apps/server/public" ]; then | |
| cp -r apps/server/public deploy/server/apps/server/public | |
| fi | |
| # Copy prisma directory for migrations | |
| if [ -d "apps/server/prisma" ]; then | |
| cp -r apps/server/prisma deploy/server/apps/server/prisma | |
| fi | |
| echo "✅ Server packaged" | |
| else | |
| echo "❌ No standalone build found!" | |
| exit 1 | |
| fi | |
| # --- Studio: static SPA --- | |
| if [ -d "apps/studio/dist" ]; then | |
| cp -r apps/studio/dist/. deploy/studio/ | |
| echo "✅ Studio packaged" | |
| else | |
| echo "❌ Studio dist not found!" | |
| exit 1 | |
| fi | |
| # --- Widgets (two-level: packages/widgets/category/name/dist) --- | |
| for dir in packages/widgets/*/*/; do | |
| if [ -d "${dir}dist" ]; then | |
| rel_path="${dir#packages/widgets/}" | |
| rel_path="${rel_path%/}" | |
| mkdir -p "deploy/widgets/${rel_path}" | |
| cp -r "${dir}dist" "deploy/widgets/${rel_path}/" | |
| fi | |
| done | |
| echo "✅ Widgets packaged" | |
| # --- Config --- | |
| cat > deploy/thingsvis.conf <<'EOF' | |
| server { | |
| listen 7050; | |
| server_name _; | |
| root /var/www/thingsvis-studio; | |
| index index.html; | |
| gzip on; | |
| gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml; | |
| location /api/ { | |
| proxy_pass http://127.0.0.1:8000; | |
| proxy_http_version 1.1; | |
| proxy_set_header Host $host; | |
| proxy_set_header X-Real-IP $remote_addr; | |
| proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | |
| proxy_set_header X-Forwarded-Proto $scheme; | |
| proxy_read_timeout 300s; | |
| client_max_body_size 50m; | |
| } | |
| location ^~ /uploads/ { | |
| proxy_pass http://127.0.0.1:8000; | |
| proxy_http_version 1.1; | |
| proxy_set_header Host $host; | |
| proxy_set_header X-Real-IP $remote_addr; | |
| proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | |
| proxy_set_header X-Forwarded-Proto $scheme; | |
| } | |
| location ^~ /widgets/ { | |
| alias /var/www/thingsvis-plugins/; | |
| } | |
| location / { | |
| try_files $uri $uri/ /index.html; | |
| } | |
| location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { | |
| expires 1y; | |
| add_header Cache-Control "public, immutable"; | |
| } | |
| } | |
| EOF | |
| # Create tarball | |
| cd deploy && tar -czf ../thingsvis-deploy.tar.gz . | |
| test -f ../thingsvis-deploy.tar.gz | |
| echo "📦 Tarball size: $(du -h ../thingsvis-deploy.tar.gz | cut -f1)" | |
| # ======================================== | |
| # Deploy to server via SSH key | |
| # ======================================== | |
| # DEPLOY_HOST_E: 目标服务器 IP 地址或域名 | |
| # DEPLOY_USER_E: SSH 登录用户名(需有 /var/www 和 /opt/thingsvis 目录写入权限) | |
| # DEPLOY_SSH_KEY_E: 部署用的 SSH 私钥(建议创建专用 deploy 用户并配置仅允许 key 登录) | |
| - name: Upload to server | |
| uses: appleboy/scp-action@v0.1.7 | |
| with: | |
| host: ${{ secrets.DEPLOY_HOST_E }} | |
| username: ${{ secrets.DEPLOY_USER_E }} | |
| key: ${{ secrets.DEPLOY_SSH_KEY_E }} | |
| port: 22 | |
| source: "thingsvis-deploy.tar.gz" | |
| target: "/tmp" | |
| # 同上:DEPLOY_HOST_E / DEPLOY_USER_E / DEPLOY_SSH_KEY_E | |
| - name: Deploy and restart services | |
| uses: appleboy/ssh-action@v1.0.3 | |
| with: | |
| host: ${{ secrets.DEPLOY_HOST_E }} | |
| username: ${{ secrets.DEPLOY_USER_E }} | |
| key: ${{ secrets.DEPLOY_SSH_KEY_E }} | |
| port: 22 | |
| script: | | |
| set -e | |
| export PATH="/usr/local/lib/nodejs/node-v16.14.2-linux-x64/bin:$PATH" | |
| echo "=== Deploying ThingsVis ===" | |
| # Ensure directories exist | |
| mkdir -p /opt/thingsvis/server | |
| mkdir -p /var/www/thingsvis-studio | |
| mkdir -p /var/www/thingsvis-plugins | |
| # Extract | |
| mkdir -p /tmp/thingsvis-deploy | |
| tar -xzf /tmp/thingsvis-deploy.tar.gz -C /tmp/thingsvis-deploy | |
| # --- Deploy Nginx Config --- | |
| cp /tmp/thingsvis-deploy/thingsvis.conf /etc/nginx/conf.d/thingsvis.conf | |
| # --- Deploy Server --- | |
| echo ">>> Deploying Server..." | |
| pm2 stop thingsvis-server 2>/dev/null || true | |
| sleep 1 | |
| rm -rf /opt/thingsvis/server/.next /opt/thingsvis/server/node_modules | |
| cp -rf /tmp/thingsvis-deploy/server/. /opt/thingsvis/server/ | |
| cd /opt/thingsvis/server | |
| # Source server env from the packaged app directory when available. | |
| if [ -f /opt/thingsvis/server/apps/server/.env ]; then | |
| set -a | |
| . /opt/thingsvis/server/apps/server/.env | |
| set +a | |
| elif [ -f /opt/thingsvis/server/.env ]; then | |
| set -a | |
| . /opt/thingsvis/server/.env | |
| set +a | |
| fi | |
| # Fail fast when required secrets are missing to avoid PM2 persisting empty env. | |
| : "${DATABASE_URL_E:?DATABASE_URL_E is required for deploy-e}" | |
| : "${AUTH_SECRET:?AUTH_SECRET is required for deploy-e}" | |
| export DATABASE_URL="$DATABASE_URL_E" | |
| # Apply database migrations | |
| cd apps/server | |
| npx prisma db push --accept-data-loss || true | |
| cd ../.. | |
| NODE_ENV=production HOSTNAME=127.0.0.1 AUTH_TRUST_HOST=true PORT=8000 \ | |
| DATABASE_URL="$DATABASE_URL" AUTH_SECRET="$AUTH_SECRET" AUTH_URL="${AUTH_URL:-http://127.0.0.1:8000}" \ | |
| pm2 start apps/server/server.js --name thingsvis-server --update-env 2>/dev/null || \ | |
| NODE_ENV=production HOSTNAME=127.0.0.1 AUTH_TRUST_HOST=true PORT=8000 \ | |
| DATABASE_URL="$DATABASE_URL" AUTH_SECRET="$AUTH_SECRET" AUTH_URL="${AUTH_URL:-http://127.0.0.1:8000}" \ | |
| pm2 restart thingsvis-server --update-env | |
| pm2 save | |
| # --- Deploy Studio --- | |
| echo ">>> Deploying Studio..." | |
| rm -rf /var/www/thingsvis-studio/* | |
| cp -rf /tmp/thingsvis-deploy/studio/. /var/www/thingsvis-studio/ | |
| # --- Deploy Widgets --- | |
| echo ">>> Deploying Widgets..." | |
| if [ -d "/tmp/thingsvis-deploy/widgets" ] && [ "$(ls -A /tmp/thingsvis-deploy/widgets 2>/dev/null)" ]; then | |
| mkdir -p /var/www/thingsvis-plugins | |
| cp -rf /tmp/thingsvis-deploy/widgets/. /var/www/thingsvis-plugins/ | |
| fi | |
| # Reload nginx | |
| nginx -t && systemctl reload nginx | |
| # Cleanup | |
| rm -rf /tmp/thingsvis-deploy /tmp/thingsvis-deploy.tar.gz | |
| echo "" | |
| echo "=== Deployment complete ===" | |
| pm2 list | |
| echo "" | |
| echo "🔗 Studio: http://${{ secrets.DEPLOY_HOST_E }}:7050/" |