Skip to content

feat(deploy): improve environment variable sourcing and validation in… #88

feat(deploy): improve environment variable sourcing and validation in…

feat(deploy): improve environment variable sourcing and validation in… #88

Workflow file for this run

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/"