guanghulab/.github/archived-workflows/deploy-to-server.yml
2026-05-10 13:12:44 +08:00

814 lines
39 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ═══════════════════════════════════════════════
# 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
# 📜 Copyright: 国作登字-2026-A-00037559
# ═══════════════════════════════════════════════
name: "🚀 铸渊 CD · 自动部署到 guanghulab.com"
# ━━━ 铸渊主权服务器部署 · ZY-SVR-002 ━━━
# 已从香港(150.109.76.244)迁移到新加坡(43.134.16.246)
# push 到 main → 校验 → rsync 同步全站到服务器 → 后端重启 → Nginx 配置 → Notion 通知
#
# 必需 GitHub Secrets参考 brain/secrets-manifest.json:
# ZY_SERVER_HOST — 铸渊主权服务器 IP (43.134.16.246)
# ZY_SERVER_USER — SSH 用户名
# ZY_SERVER_KEY — SSH 私钥(完整 PEM 内容)
# ZY_SERVER_PATH — 网站根目录(/opt/zhuyuan
#
# 可选 Secrets:
# ZY_NOTION_TOKEN — Notion 集成 token
# ZY_NOTION_CHANGELOG_DB — Notion 变更日志数据库 ID
on:
push:
branches: [main]
paths-ignore:
- '.github/persona-brain/**'
- 'broadcasts-outbox/**'
- 'syslog-inbox/**'
- 'syslog-processed/**'
- 'signal-log/**'
- 'dev-nodes/**'
workflow_dispatch:
permissions:
contents: read
jobs:
# ═══════════════════════════════════════════════════════════════
# Job 1: 部署前校验
# ═══════════════════════════════════════════════════════════════
validate:
name: "🔍 部署前校验"
runs-on: ubuntu-latest
steps:
- name: 📥 检出代码
uses: actions/checkout@v4
- name: 🟢 配置 Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: 📦 安装依赖(如需要)
run: |
if [ -f "package-lock.json" ] && grep -q '"glob"' package.json; then
npm ci --ignore-scripts || npm install --ignore-scripts || echo "⚠️ 依赖安装失败,继续执行"
fi
- name: 🔍 HLI 契约校验
run: |
if [ -f "scripts/contract-check.js" ]; then
echo "🔍 执行 HLI 契约校验..."
node scripts/contract-check.js
else
echo "⏭️ scripts/contract-check.js 不存在,跳过 HLI 校验"
fi
- name: 📋 模块指纹检查
run: |
if [ -f "scripts/zhuyuan-module-protocol.js" ]; then
echo "🔍 执行模块指纹检查..."
node scripts/zhuyuan-module-protocol.js status
else
echo "⏭️ scripts/zhuyuan-module-protocol.js 不存在,跳过模块检查"
fi
- name: ✅ 校验汇总
run: |
echo "### ✅ 部署前校验通过" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- HLI 契约校验: ✅" >> $GITHUB_STEP_SUMMARY
echo "- 模块指纹检查: ✅" >> $GITHUB_STEP_SUMMARY
echo "- 提交: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
# ═══════════════════════════════════════════════════════════════
# Job 2: rsync 同步到服务器
# ═══════════════════════════════════════════════════════════════
deploy:
name: "🚀 部署到服务器"
needs: validate
runs-on: ubuntu-latest
steps:
- name: 🔐 检查必需 Secrets
run: |
# ZY_SERVER_* 密钥(铸渊主权服务器 · 参考 brain/secrets-manifest.json
if [ -n "${{ secrets.ZY_SERVER_HOST }}" ]; then
echo "SERVER_HOST=${{ secrets.ZY_SERVER_HOST }}" >> $GITHUB_ENV
echo "SERVER_USER=${{ secrets.ZY_SERVER_USER }}" >> $GITHUB_ENV
echo "SERVER_KEY<<KEYEOF" >> $GITHUB_ENV
echo "${{ secrets.ZY_SERVER_KEY }}" >> $GITHUB_ENV
echo "KEYEOF" >> $GITHUB_ENV
echo "SERVER_PATH=${{ secrets.ZY_SERVER_PATH }}" >> $GITHUB_ENV
echo "✅ 使用铸渊主权服务器密钥 (ZY_SERVER_*)"
else
echo "❌ 未配置服务器部署密钥"
echo ""
echo "请在仓库 Settings → Secrets and variables → Actions 中添加:"
echo " ZY_SERVER_HOST — 铸渊主权服务器 IP (43.134.16.246)"
echo " ZY_SERVER_USER — SSH 用户名"
echo " ZY_SERVER_KEY — SSH 私钥(完整 PEM 内容)"
echo " ZY_SERVER_PATH — 部署根目录 (/opt/zhuyuan)"
echo ""
echo "### ❌ 部署失败:缺少服务器部署密钥" >> $GITHUB_STEP_SUMMARY
echo "请配置 ZY_SERVER_HOST / ZY_SERVER_USER / ZY_SERVER_KEY / ZY_SERVER_PATH" >> $GITHUB_STEP_SUMMARY
exit 1
fi
# 验证必需字段
MISSING=""
if [ -z "${{ secrets.ZY_SERVER_HOST }}" ]; then MISSING="${MISSING} HOST"; fi
if [ -z "${{ secrets.ZY_SERVER_USER }}" ]; then MISSING="${MISSING} USER"; fi
if [ -z "${{ secrets.ZY_SERVER_KEY }}" ]; then MISSING="${MISSING} KEY"; fi
if [ -z "${{ secrets.ZY_SERVER_PATH }}" ]; then MISSING="${MISSING} PATH"; fi
if [ -n "$MISSING" ]; then
echo "❌ 缺少必需密钥:${MISSING}"
exit 1
fi
echo "✅ 所有必需 Secrets 已配置"
- name: 📥 检出代码
uses: actions/checkout@v4
- name: 📦 确保本地 rsync 已安装
run: |
if ! command -v rsync &>/dev/null; then
sudo apt-get update -qq && sudo apt-get install -y rsync
fi
rsync --version | head -1
- name: 🔑 配置 SSH 连接
run: |
mkdir -p ~/.ssh
echo "${{ env.SERVER_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H "${{ env.SERVER_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null || {
echo "⚠️ ssh-keyscan 失败,尝试使用 StrictHostKeyChecking=no 作为后备"
}
- name: 📦 确保远程服务器 rsync 已安装
run: |
echo "🔍 检查远程服务器 rsync..."
ssh -i ~/.ssh/deploy_key ${{ env.SERVER_USER }}@${{ env.SERVER_HOST }} \
"command -v rsync || { echo '⚠️ 远程服务器未安装 rsync尝试安装...' && \
(sudo apt-get update -qq && sudo apt-get install -y rsync 2>/dev/null || \
sudo yum install -y rsync 2>/dev/null || \
{ echo '❌ 无法自动安装 rsync可能缺少 sudo 权限)'; \
echo '请手动在服务器上安装: sudo apt-get install rsync 或 sudo yum install rsync'; \
exit 1; }); }"
echo "✅ 远程服务器 rsync 检查完成"
- name: 📁 确保远程部署目录存在
run: |
echo "🔍 检查远程部署目录..."
ssh -i ~/.ssh/deploy_key \
"${{ env.SERVER_USER }}@${{ env.SERVER_HOST }}" \
"mkdir -p '${{ env.SERVER_PATH }}'" || {
echo "❌ 无法创建远程部署目录SSH 连接失败或权限不足)"
echo "请确认 ZY_SERVER_PATH 设置正确且 ZY_SERVER_USER 有写入权限"
exit 1
}
echo "✅ 远程部署目录已就绪"
- name: 🔒 部署前安全检查
run: |
if [ ! -f "docs/index.html" ]; then
echo "❌ docs/index.html 不存在,中止部署以防清空生产环境"
exit 1
fi
FILE_COUNT=$(find . -type f \
-not -path './.git/*' \
-not -path './.github/*' \
-not -path '*/node_modules/*' \
| wc -l)
if [ "$FILE_COUNT" -lt 5 ]; then
echo "❌ 仓库文件数异常(仅 ${FILE_COUNT} 个),中止部署"
exit 1
fi
echo "✅ 仓库包含 ${FILE_COUNT} 个文件,安全检查通过"
- name: 🚀 rsync 同步全站到服务器
run: |
echo "📡 开始同步全站到 ${{ env.SERVER_HOST }}:${{ env.SERVER_PATH }}"
rsync -avz --delete-after \
--exclude '.git' \
--exclude '.github' \
--exclude 'node_modules' \
--exclude 'syslog-inbox' \
--exclude 'syslog-processed' \
--exclude 'broadcasts-outbox' \
--exclude 'dev-nodes' \
--exclude 'signal-log' \
--exclude 'tests' \
--exclude 'reports' \
--exclude 'scripts' \
--exclude 'guanghulab-main' \
--exclude '*.bak' \
--exclude '*.bak2' \
--exclude '*_副本' \
--exclude '*_副本/' \
--exclude '*_副本.*' \
--exclude '.DS_Store' \
--exclude '.env' \
--exclude 'package-lock.json' \
--exclude 'tsconfig.json' \
--exclude 'next.config.ts' \
--exclude 'postcss.config.mjs' \
--exclude 'jest.smoke.config.js' \
--exclude 'ecosystem.config.js' \
--exclude '.gitignore' \
--exclude 'CNAME' \
--exclude '*.zip' \
-e "ssh -i ~/.ssh/deploy_key" \
./ \
${{ env.SERVER_USER }}@${{ env.SERVER_HOST }}:${{ env.SERVER_PATH }}/
echo "✅ 全站同步完成"
- name: 🌐 注入生产环境标识
run: |
echo "🌐 Setting HOLOLAKE_ENV=production on server..."
ssh -i ~/.ssh/deploy_key \
"${{ env.SERVER_USER }}@${{ env.SERVER_HOST }}" "
DP='${{ env.SERVER_PATH }}'
# Create environment config file for frontend detection
echo '{\"env\":\"production\",\"domain\":\"guanghulab.com\",\"deployed_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"commit\":\"${{ github.sha }}\"}' > \"\$DP/docs/env.json\"
echo '{\"env\":\"production\",\"domain\":\"guanghulab.com\",\"deployed_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"commit\":\"${{ github.sha }}\"}' > \"\$DP/env.json\"
"
echo "✅ 环境标识已注入"
- name: 📄 复制首页到站点根目录
run: |
echo "📄 复制 docs/index.html → 站点根目录 index.html"
ssh -i ~/.ssh/deploy_key \
"${{ env.SERVER_USER }}@${{ env.SERVER_HOST }}" "
cp '${{ env.SERVER_PATH }}/docs/index.html' '${{ env.SERVER_PATH }}/index.html'
# 同步 docs 下的其他静态资源到根目录
cp '${{ env.SERVER_PATH }}/docs/.nojekyll' '${{ env.SERVER_PATH }}/.nojekyll' 2>/dev/null || true
"
echo "✅ 首页已部署到根目录"
- name: 🔗 模块路由别名symlink 补全)
continue-on-error: true
run: |
echo "🔗 创建模块路由别名..."
ssh -i ~/.ssh/deploy_key \
"${{ env.SERVER_USER }}@${{ env.SERVER_HOST }}" "
DP='${{ env.SERVER_PATH }}'
# dashboard → docs/dashboard仪表盘在 docs/ 子目录下)
if [ -d \"\$DP/docs/dashboard\" ] && [ ! -e \"\$DP/dashboard\" ]; then
ln -s \"\$DP/docs/dashboard\" \"\$DP/dashboard\"
echo '✅ dashboard → docs/dashboard'
elif [ -L \"\$DP/dashboard\" ]; then
echo '✅ dashboard symlink 已存在'
elif [ -d \"\$DP/dashboard\" ]; then
echo '✅ dashboard 目录已存在'
else
echo '⚠️ docs/dashboard 不存在,跳过'
fi
# devboard → modules/devboard开发者看板在 modules/ 子目录下)
if [ -d \"\$DP/modules/devboard\" ] && [ ! -e \"\$DP/devboard\" ]; then
ln -s \"\$DP/modules/devboard\" \"\$DP/devboard\"
echo '✅ devboard → modules/devboard'
elif [ -L \"\$DP/devboard\" ]; then
echo '✅ devboard symlink 已存在'
elif [ -d \"\$DP/devboard\" ]; then
echo '✅ devboard 目录已存在'
else
echo '⚠️ modules/devboard 不存在,跳过'
fi
"
echo "✅ 模块路由别名处理完成"
- name: 🔧 后端服务部署
continue-on-error: true
run: |
echo "🔧 检查并部署后端服务..."
ssh -i ~/.ssh/deploy_key \
"${{ env.SERVER_USER }}@${{ env.SERVER_HOST }}" "
# 检查 Node.js 和 pm2 是否可用
if ! command -v node &>/dev/null; then
echo '⏭️ Node.js 未安装,跳过后端部署'
exit 0
fi
if ! command -v pm2 &>/dev/null; then
echo '⏭️ pm2 未安装,跳过后端服务管理'
exit 0
fi
# 部署 API 代理服务AI 聊天代理,端口 3721
if [ -f '${{ env.SERVER_PATH }}/backend-integration/api-proxy.js' ]; then
echo '📦 安装 API 代理依赖...'
cd '${{ env.SERVER_PATH }}/backend-integration'
npm install --production 2>/dev/null || echo '⚠️ api-proxy npm install 失败(可能无 package.json继续'
echo '🔄 重启 API 代理服务...'
export YUNWU_API_KEY='${{ secrets.ZY_LLM_API_KEY }}'
export DEEPSEEK_API_KEY='${{ secrets.ZY_LLM_API_KEY }}'
pm2 delete guanghulab-proxy 2>/dev/null || true
YUNWU_API_KEY='${{ secrets.ZY_LLM_API_KEY }}' DEEPSEEK_API_KEY='${{ secrets.ZY_LLM_API_KEY }}' \
pm2 start api-proxy.js --name guanghulab-proxy --update-env 2>/dev/null || \
echo '⚠️ API 代理 pm2 启动失败'
fi
# 部署 backend 服务Express API端口 3000
if [ -f '${{ env.SERVER_PATH }}/backend/server.js' ]; then
echo '📦 安装 backend 依赖...'
cd '${{ env.SERVER_PATH }}/backend'
npm install --production 2>/dev/null || echo '⚠️ backend npm install 失败'
echo '🔄 重启 backend 服务...'
pm2 restart guanghulab-backend 2>/dev/null || \
pm2 start server.js --name guanghulab-backend 2>/dev/null || \
echo '⚠️ backend pm2 启动失败'
fi
# 部署 status-board WebSocket 服务(端口 8080
if [ -f '${{ env.SERVER_PATH }}/status-board/mock-ws-server.js' ]; then
echo '🔄 重启 WebSocket 服务...'
cd '${{ env.SERVER_PATH }}/status-board'
npm install --production 2>/dev/null || true
pm2 restart guanghulab-ws 2>/dev/null || \
pm2 start mock-ws-server.js --name guanghulab-ws 2>/dev/null || \
echo '⚠️ WebSocket pm2 启动失败'
fi
# 部署 src HLI 中间层(端口 3001对应 Nginx /api/v1/ 路由)
# 注意guanghulab-backend 已占用端口 3000HLI 使用 3001 避免冲突
if [ -f '${{ env.SERVER_PATH }}/src/index.js' ]; then
echo '📦 安装 src 依赖...'
cd '${{ env.SERVER_PATH }}'
npm install --production 2>/dev/null || echo '⚠️ src npm install 失败'
echo '🔄 重启 HLI 服务(端口 3001...'
pm2 delete guanghulab 2>/dev/null || true
PORT=3001 pm2 start src/index.js --name guanghulab --update-env 2>/dev/null || \
echo '⚠️ src pm2 启动失败'
fi
# 部署 Persona Studio 后端服务(端口 3002
if [ -f '${{ env.SERVER_PATH }}/persona-studio/backend/server.js' ]; then
echo '📦 安装 persona-studio 依赖...'
cd '${{ env.SERVER_PATH }}/persona-studio/backend'
npm install --production 2>/dev/null || echo '⚠️ persona-studio npm install 失败'
echo '🔄 重启 persona-studio 服务...'
export MODEL_API_KEY='${{ secrets.ZY_LLM_API_KEY }}'
pm2 delete persona-studio 2>/dev/null || true
MODEL_API_KEY='${{ secrets.ZY_LLM_API_KEY }}' \
pm2 start server.js --name persona-studio --update-env 2>/dev/null || \
echo '⚠️ persona-studio pm2 启动失败'
fi
pm2 save || echo '⚠️ pm2 save 失败,重启后进程可能不会自动启动'
# 设置开机自启(仅首次需要,后续无操作)
pm2 startup 2>/dev/null | grep -o 'sudo.*' | bash 2>/dev/null || true
echo '✅ 后端服务部署完成'
"
- name: 🌐 更新 Nginx 配置
continue-on-error: true
run: |
echo "🔧 检查 Nginx 配置..."
ssh -i ~/.ssh/deploy_key \
"${{ env.SERVER_USER }}@${{ env.SERVER_HOST }}" "
# 检查 Nginx 是否安装
if ! command -v nginx &>/dev/null; then
echo '⏭️ Nginx 未安装,跳过配置更新'
exit 0
fi
NGINX_CHANGED=false
# 一次性迁移:更新 root 路径(从旧的 status-board 子目录改为站点根目录)
if grep -q 'root.*guanghulab/status-board' /etc/nginx/sites-available/default 2>/dev/null || \
grep -q 'root.*guanghulab/status-board' /etc/nginx/conf.d/*.conf 2>/dev/null; then
echo '🔄 更新 Nginx root 路径status-board → 站点根)...'
sudo sed -i 's|root.*guanghulab/status-board[^;]*|root ${{ env.SERVER_PATH }}|g' \
/etc/nginx/sites-available/default 2>/dev/null || true
sudo sed -i 's|root.*guanghulab/status-board[^;]*|root ${{ env.SERVER_PATH }}|g' \
/etc/nginx/conf.d/*.conf 2>/dev/null || true
NGINX_CHANGED=true
echo '✅ Nginx root 路径已更新status-board 迁移)'
fi
# 一次性迁移:修正 root 路径(从旧残留 hololake-frontend 改为 guanghulab
# 根因ZY-NGINX-ROOTFIX-2026-0325-001 · DEV-004 之之诊断
if grep -q 'root.*/var/www/hololake-frontend' /etc/nginx/sites-available/default 2>/dev/null || \
grep -q 'root.*/var/www/hololake-frontend' /etc/nginx/conf.d/*.conf 2>/dev/null; then
echo '🔄 更新 Nginx root 路径hololake-frontend → guanghulab...'
sudo sed -i 's|root /var/www/hololake-frontend|root ${{ env.SERVER_PATH }}|g' \
/etc/nginx/sites-available/default 2>/dev/null || true
sudo sed -i 's|root /var/www/hololake-frontend|root ${{ env.SERVER_PATH }}|g' \
/etc/nginx/conf.d/*.conf 2>/dev/null || true
NGINX_CHANGED=true
echo '✅ Nginx root 路径已更新hololake-frontend → guanghulab'
fi
# 验证 root 路径确实正确(防御性检查)
if ! grep -q "root ${{ env.SERVER_PATH }}" /etc/nginx/conf.d/*.conf 2>/dev/null && \
! grep -q "root ${{ env.SERVER_PATH }}" /etc/nginx/sites-available/default 2>/dev/null; then
echo '⚠️ Nginx root 路径可能未正确设置,请手动检查'
else
echo '✅ Nginx root 路径正确,指向 ${{ env.SERVER_PATH }}'
fi
# 配置 API + WebSocket 反向代理
if ! grep -rq 'location /api/' /etc/nginx/sites-available/ /etc/nginx/conf.d/ /etc/nginx/snippets/ 2>/dev/null; then
echo '🔄 添加 API + WebSocket 反向代理...'
sudo mkdir -p /etc/nginx/snippets
# 注意:\$ 在本地 shell 中转义为 $,远程 shell 的单引号保护不再展开,
# 最终写入文件的 Nginx 变量为 $host、$http_upgrade 等
printf '%s\n' \
'# guanghulab 反向代理(由 CD 工作流自动生成)' \
'' \
'# AI 聊天 API 代理 → api-proxy.js 端口 3721SSE 流式支持)' \
'location /api/chat {' \
' proxy_pass http://127.0.0.1:3721/api/chat;' \
' 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_set_header Connection "";' \
' proxy_buffering off;' \
' proxy_cache off;' \
' chunked_transfer_encoding on;' \
' proxy_read_timeout 90s;' \
' proxy_send_timeout 60s;' \
'}' \
'' \
'location /api/models {' \
' proxy_pass http://127.0.0.1:3721/api/models;' \
' proxy_http_version 1.1;' \
' proxy_set_header Host \$host;' \
' proxy_set_header X-Real-IP \$remote_addr;' \
'}' \
'' \
'location /api/health {' \
' proxy_pass http://127.0.0.1:3721/api/health;' \
' proxy_http_version 1.1;' \
' proxy_set_header Host \$host;' \
' proxy_set_header X-Real-IP \$remote_addr;' \
'}' \
'' \
'# Persona Studio API → persona-studio 后端端口 3002含 CORS 跨域支持)' \
'location /api/ps/ {' \
' add_header Access-Control-Allow-Origin * always;' \
' add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;' \
' add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;' \
' if (\$request_method = OPTIONS) {' \
' return 204;' \
' }' \
' proxy_pass http://127.0.0.1:3002/api/ps/;' \
' 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 90s;' \
'}' \
'' \
'# Persona Studio 前端静态文件' \
'location /persona-studio/ {' \
' proxy_pass http://127.0.0.1:3002/persona-studio/;' \
' proxy_http_version 1.1;' \
' proxy_set_header Host \$host;' \
' proxy_set_header X-Real-IP \$remote_addr;' \
'}' \
'' \
'# 其他 API → Express 后端端口 3000' \
'location /api/ {' \
' proxy_pass http://127.0.0.1:3000/api/;' \
' 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_cache_bypass \$http_upgrade;' \
'}' \
'' \
'# Persona Studio WebSocket → 预览进度推送端口 3002' \
'location /ws/preview {' \
' proxy_pass http://127.0.0.1:3002;' \
' proxy_http_version 1.1;' \
' proxy_set_header Upgrade \$http_upgrade;' \
' proxy_set_header Connection \"Upgrade\";' \
' 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_read_timeout 86400;' \
'}' \
'' \
'# WebSocket 反向代理 → 看板实时推送端口 8080' \
'location /ws {' \
' proxy_pass http://127.0.0.1:8080;' \
' proxy_http_version 1.1;' \
' proxy_set_header Upgrade \$http_upgrade;' \
' proxy_set_header Connection \"Upgrade\";' \
' 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_read_timeout 86400;' \
'}' \
| sudo tee /etc/nginx/snippets/guanghulab-proxy.conf > /dev/null
# 在 server 块中加入 include 指令(仅搜索 sites-available 避免 symlink 重复)
NGINX_MAIN=\$(grep -rl 'server_name' /etc/nginx/sites-available/ 2>/dev/null | head -1)
if [ -z \"\$NGINX_MAIN\" ]; then
NGINX_MAIN='/etc/nginx/sites-available/default'
fi
if [ -f \"\$NGINX_MAIN\" ] && ! grep -q 'guanghulab-proxy' \"\$NGINX_MAIN\" 2>/dev/null; then
sudo sed -i '/server_name/a\\ include /etc/nginx/snippets/guanghulab-proxy.conf;' \"\$NGINX_MAIN\" 2>/dev/null && \
NGINX_CHANGED=true && echo '✅ 反向代理已自动配置' || \
echo '⚠️ 自动配置失败,请手动在 Nginx server 块中加入: include /etc/nginx/snippets/guanghulab-proxy.conf;'
fi
else
echo '✅ API 反向代理已存在,无需修改'
fi
# 补建 _shared 目录(沙盒共享资源)
if [ ! -d /var/www/_shared ]; then
sudo mkdir -p /var/www/_shared
sudo chmod 755 /var/www/_shared
echo '✅ _shared 目录已创建'
fi
# 沙盒 alias 路由补充DEV-004, DEV-005
# 根因ZY-NGINX-ROOTFIX-2026-0325-001 · 沙盒模块需通过域名访问
SANDBOX_CONF='/etc/nginx/snippets/guanghulab-sandbox.conf'
if [ ! -f \"\$SANDBOX_CONF\" ]; then
printf '%s\n' \
'# guanghulab 沙盒路由(由 CD 工作流自动生成)' \
'' \
'# DEV-004 之之 · 沙盒路由' \
'location /DEV-004/ {' \
' alias /var/www/DEV-004/;' \
' index index.html;' \
' try_files \$uri \$uri/ =404;' \
'}' \
'' \
'# DEV-005 小草莓 · 沙盒路由' \
'location /DEV-005/ {' \
' alias /var/www/DEV-005/;' \
' index index.html;' \
' try_files \$uri \$uri/ =404;' \
'}' \
| sudo tee \"\$SANDBOX_CONF\" > /dev/null
# 在 server 块中加入 sandbox include 指令
NGINX_MAIN=\$(grep -rl 'server_name' /etc/nginx/sites-available/ 2>/dev/null | head -1)
if [ -z \"\$NGINX_MAIN\" ]; then
NGINX_MAIN='/etc/nginx/sites-available/default'
fi
if [ -f \"\$NGINX_MAIN\" ] && ! grep -q 'guanghulab-sandbox' \"\$NGINX_MAIN\" 2>/dev/null; then
sudo sed -i '/server_name/a\\ include /etc/nginx/snippets/guanghulab-sandbox.conf;' \"\$NGINX_MAIN\" 2>/dev/null && \
NGINX_CHANGED=true && echo '✅ 沙盒路由已自动配置' || \
echo '⚠️ 沙盒路由自动配置失败,请手动添加'
fi
else
echo '✅ 沙盒路由配置已存在'
fi
# 如果有变更,验证并重新加载 Nginx
if [ \"\$NGINX_CHANGED\" = true ]; then
sudo nginx -t && sudo systemctl reload nginx && echo '✅ Nginx 配置已重新加载' || \
echo '⚠️ Nginx 配置验证失败,请手动检查'
fi
"
- name: 🔍 验证部署
run: |
ssh -i ~/.ssh/deploy_key \
${{ env.SERVER_USER }}@${{ env.SERVER_HOST }} \
"echo '✅ 服务器连接正常' && \
echo '--- 站点根目录 ---' && \
ls -la ${{ env.SERVER_PATH }}/ | head -30 && \
echo '--- docs/ ---' && \
ls ${{ env.SERVER_PATH }}/docs/ 2>/dev/null || echo '(docs/ 不存在)' && \
echo '--- status-board/ ---' && \
ls ${{ env.SERVER_PATH }}/status-board/ 2>/dev/null || echo '(status-board/ 不存在)' && \
echo '--- cost-control/ ---' && \
ls ${{ env.SERVER_PATH }}/cost-control/ 2>/dev/null || echo '(cost-control/ 不存在)' && \
echo '--- devboard/ ---' && \
ls ${{ env.SERVER_PATH }}/devboard/ 2>/dev/null || echo '(devboard/ 不存在)' && \
echo '--- persona-studio/ ---' && \
ls ${{ env.SERVER_PATH }}/persona-studio/ 2>/dev/null || echo '(persona-studio/ 不存在)'"
- name: 🏥 部署后冒烟测试
continue-on-error: true
run: |
echo "🏥 部署后全模块冒烟测试..."
ssh -i ~/.ssh/deploy_key \
"${{ env.SERVER_USER }}@${{ env.SERVER_HOST }}" "
DP='${{ env.SERVER_PATH }}'
PASSED=0
FAILED=0
RESULTS=''
check_module() {
local name=\$1
local path=\$2
if [ -f \"\$DP/\$path/index.html\" ] || [ -f \"\$DP/\$path/index.htm\" ]; then
PASSED=\$((PASSED+1))
RESULTS=\"\${RESULTS}✅ \${name} (\${path}/index.html)\n\"
elif [ -d \"\$DP/\$path\" ]; then
PASSED=\$((PASSED+1))
RESULTS=\"\${RESULTS}⚠️ \${name} (目录存在,无 index.html)\n\"
else
FAILED=\$((FAILED+1))
RESULTS=\"\${RESULTS}❌ \${name} (\${path}/ 不存在)\n\"
fi
}
check_module '主站首页' 'docs'
check_module '系统状态看板' 'status-board'
check_module '成本控制' 'cost-control'
check_module '开发者看板' 'devboard'
check_module '光湖集成壳' 'app'
check_module 'Persona Studio' 'persona-studio'
check_module '后端服务' 'backend'
check_module '用户中心' 'm05-user-center'
check_module '对话界面' 'm07-dialogue-ui'
check_module '云服务' 'm10-cloud'
check_module '模块管理' 'm11-module'
check_module '云盘' 'm15-cloud-drive'
check_module '登录' 'm01-login'
check_module '健康检查' 'm18-health-check'
check_module '冷启动' 'coldstart'
check_module '仪表盘' 'dashboard'
check_module '门户' 'portal'
check_module '通知' 'notification'
check_module '帮助中心' 'help-center'
check_module '前端' 'frontend'
echo ''
printf \"\$RESULTS\"
echo ''
echo \"━━━ 冒烟测试结果:\${PASSED} 通过 / \${FAILED} 失败 ━━━\"
# 写入部署状态文件
TIMESTAMP=\$(date -u '+%Y-%m-%dT%H:%M:%SZ')
if command -v jq &>/dev/null; then
jq -n --arg ts \"\$TIMESTAMP\" --arg commit \"${{ github.sha }}\" \
--argjson passed \"\$PASSED\" --argjson failed \"\$FAILED\" \
'{last_deploy: \$ts, commit: \$commit, modules_passed: \$passed, modules_failed: \$failed}' \
> \"\$DP/data/deploy-status.json\" 2>/dev/null || true
else
printf '{\"last_deploy\":\"%s\",\"commit\":\"%s\",\"modules_passed\":%d,\"modules_failed\":%d}' \
\"\$TIMESTAMP\" \"${{ github.sha }}\" \"\$PASSED\" \"\$FAILED\" \
> \"\$DP/data/deploy-status.json\" 2>/dev/null || true
fi
"
echo "✅ 冒烟测试完成"
- name: 👤 人类使用侧验证
continue-on-error: true
run: |
echo "👤 人类使用侧验证(核心!系统测跑通 ≠ 人类测跑通)..."
echo "等待 10 秒让服务启动..."
sleep 10
HUMAN_CHECK_PASSED=0
HUMAN_CHECK_FAILED=0
# 验证主站可访问
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 \
"http://${{ env.SERVER_HOST }}" 2>/dev/null || echo "000")
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "301" ] || [ "$HTTP_CODE" = "302" ]; then
echo "✅ 主站 HTTP 响应: $HTTP_CODE"
HUMAN_CHECK_PASSED=$((HUMAN_CHECK_PASSED+1))
else
echo "❌ 主站 HTTP 响应: $HTTP_CODE人类无法访问"
HUMAN_CHECK_FAILED=$((HUMAN_CHECK_FAILED+1))
fi
# 验证 Persona Studio 可访问
PS_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 \
"http://${{ env.SERVER_HOST }}/persona-studio/" 2>/dev/null || echo "000")
if [ "$PS_CODE" = "200" ] || [ "$PS_CODE" = "301" ] || [ "$PS_CODE" = "302" ]; then
echo "✅ Persona Studio HTTP 响应: $PS_CODE"
HUMAN_CHECK_PASSED=$((HUMAN_CHECK_PASSED+1))
else
echo "⚠️ Persona Studio HTTP 响应: $PS_CODE"
HUMAN_CHECK_FAILED=$((HUMAN_CHECK_FAILED+1))
fi
# 验证后端 API 健康
API_CODE=$(ssh -i ~/.ssh/deploy_key \
"${{ env.SERVER_USER }}@${{ env.SERVER_HOST }}" \
"curl -s -o /dev/null -w '%{http_code}' --max-time 5 http://localhost:3002/api/ps/health 2>/dev/null || echo '000'" \
2>/dev/null || echo "000")
if [ "$API_CODE" = "200" ]; then
echo "✅ Persona Studio API 健康: $API_CODE"
HUMAN_CHECK_PASSED=$((HUMAN_CHECK_PASSED+1))
else
echo "⚠️ Persona Studio API 响应: $API_CODE"
HUMAN_CHECK_FAILED=$((HUMAN_CHECK_FAILED+1))
fi
echo ""
echo "━━━ 人类使用侧验证结果:${HUMAN_CHECK_PASSED} 通过 / ${HUMAN_CHECK_FAILED} 异常 ━━━"
echo "### 👤 人类使用侧验证" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- 通过: ${HUMAN_CHECK_PASSED}" >> $GITHUB_STEP_SUMMARY
echo "- 异常: ${HUMAN_CHECK_FAILED}" >> $GITHUB_STEP_SUMMARY
- name: 📝 部署汇总
run: |
echo "### 🚀 全站部署完成" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- 同步范围: 全仓库(含排除规则)" >> $GITHUB_STEP_SUMMARY
echo "- 目标: \`${{ env.SERVER_HOST }}:${{ env.SERVER_PATH }}\`" >> $GITHUB_STEP_SUMMARY
echo "- 提交: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "- 时间: $(TZ=Asia/Shanghai date '+%Y-%m-%d %H:%M CST')" >> $GITHUB_STEP_SUMMARY
echo "- 人类使用侧验证: 已执行" >> $GITHUB_STEP_SUMMARY
# ═══════════════════════════════════════════════════════════════
# Job 3: Notion 部署通知
# ═══════════════════════════════════════════════════════════════
notify:
name: "📡 部署通知 → Notion"
needs: deploy
runs-on: ubuntu-latest
if: success()
steps:
- name: 📥 检出代码
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: 🟢 配置 Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: 📡 推送部署记录到 Notion
env:
NOTION_TOKEN: ${{ secrets.ZY_NOTION_TOKEN }}
CHANGES_DB_ID: ${{ secrets.ZY_NOTION_CHANGELOG_DB }}
EVENT_TYPE: commit
COMMIT_SHA: ${{ github.sha }}
COMMIT_MSG: "[CD 部署] ${{ github.event.head_commit.message }}"
COMMITTER: ${{ github.event.head_commit.author.name || github.actor }}
COMMIT_TIMESTAMP: ${{ github.event.head_commit.timestamp }}
BRANCH: ${{ github.ref_name }}
run: |
CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || true)
export CHANGED_FILES="$CHANGED"
if [ -f "scripts/notion-bridge.js" ]; then
node scripts/notion-bridge.js changes
echo "✅ Notion 部署通知已发送"
else
echo "⏭️ scripts/notion-bridge.js 不存在,跳过 Notion 通知"
fi
# ━━━ 🧬 双端神经系统·自报告 ━━━
- name: "🧬 写入神经自报告"
if: always()
run: |
WORKFLOW_ID="cd-deploy"
BRAIN="AG-SY-01"
REPORT_DIR="data/neural-reports/cd-deploy"
TIMESTAMP=$(TZ=Asia/Shanghai date +%Y-%m-%d-%H%M)
STATUS="${{ job.status }}"
mkdir -p "$REPORT_DIR"
cat > "${REPORT_DIR}/${WORKFLOW_ID}-${TIMESTAMP}.json" << EOF
{
"workflow_id": "${WORKFLOW_ID}",
"run_id": "${{ github.run_id }}",
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"status": "${STATUS}",
"brain": "${BRAIN}",
"event": "${{ github.event_name }}",
"branch": "${{ github.ref_name }}",
"commit": "${{ github.sha }}"
}
EOF
git config user.name "zhuyuan-bot"
git config user.email "zhuyuan@guanghulab.com"
git add "$REPORT_DIR/"
git diff --cached --quiet || \
git commit -m "🧬 ${WORKFLOW_ID} 自报告 · ${TIMESTAMP} [skip ci]"
git push || echo "⚠️ 自报告push失败不阻断主流程"
# ━━━ 📡 指令回执·自动同步 ━━━
- name: "📡 同步回执到 Notion"
if: always()
env:
NOTION_API_KEY: ${{ secrets.ZY_NOTION_TOKEN }}
RECEIPT_DB_ID: ${{ secrets.ZY_NOTION_RECEIPT_DB }}
run: |
node scripts/neural/write-receipt-to-notion.js \
--instruction-id "${{ github.event.inputs.instruction_id || 'AUTO' }}" \
--status "${{ job.status }}" \
--workflow "cd-deploy" \
--summary "cd-deploy 自动执行" \
--related-agent "AG-SY-01"