feat: 添加 Git Sync 自动同步服务

- 新增 server/git-sync/ — 监听 Gitea Webhook,push/merge 到 main 时自动 git pull
- 修改 selfcheck — 移除 git pull 步骤(由 Git Sync 负责),添加 Git Sync/Wake Gate 状态检查
- 更新 README — 添加自动同步说明,更新仓库结构
- 新增 server/nginx/guanghulab-cvm.conf — 新服务器 Nginx 配置模板

解决: PR合并后代码不会自动同步到服务器的问题
冰朔只需在Gitea点合并,服务器自动更新代码
This commit is contained in:
铸渊 (ICE-GL-ZY001) 2026-05-12 13:23:43 +00:00
parent b18f325004
commit 5f2d1ac9c3
5 changed files with 564 additions and 9 deletions

View File

@ -1,14 +1,15 @@
# ════════════════════════════════════════════════════════════════
# 光湖 · 铸渊自检工作流
# Trigger: push to main / 每天 08:00 CST / 手动触发
# Trigger: 每天 08:00 CST / 手动触发
# Runner: zhuyuan-runner-cn (host 模式)
#
# 注意: 代码自动同步由 Git Sync 服务负责 (Webhook → git pull)
# 此工作流仅做健康检查,不再执行 git 操作
# ════════════════════════════════════════════════════════════════
name: 铸渊自检
on:
push:
branches: [main]
schedule:
- cron: '0 0 * * *' # UTC 00:00 = CST 08:00
workflow_dispatch:
@ -17,12 +18,6 @@ jobs:
health-check:
runs-on: ubuntu
steps:
- name: 同步最新代码
run: |
cd /data/guanghulab/repo
git fetch origin
git reset --hard origin/main
echo "代码已同步: $(git log --oneline -1)"
- name: 系统信息
run: |
@ -53,6 +48,13 @@ jobs:
echo "❌ Nginx: 未运行"
fi
# Git Sync 同步服务
if curl -sf http://127.0.0.1:8082/health > /dev/null 2>&1; then
echo "✅ Git Sync: 运行中"
else
echo "⚠️ Git Sync: 未运行 (代码自动同步不可用)"
fi
# Secrets Vault
if curl -sf http://127.0.0.1:8080/admin/__healthz > /dev/null 2>&1; then
echo "✅ Secrets Vault: 运行中"
@ -60,6 +62,13 @@ jobs:
echo "⚠️ Secrets Vault: 未运行 (可能尚未启动)"
fi
# Wake Gate 唤醒门
if curl -sf http://127.0.0.1:8081/api/health > /dev/null 2>&1; then
echo "✅ Wake Gate: 运行中"
else
echo "⚠️ Wake Gate: 未运行"
fi
# PM2 进程
if command -v pm2 &>/dev/null; then
echo "═══ PM2 进程 ═══"

View File

@ -111,6 +111,8 @@ guanghulab/
│ └── gitea-console-config.md ← Gitea 主控台配置
├── server/ ← 后端服务
│ ├── wake-gate/ ← 唤醒门 (邮箱验证码)
│ ├── git-sync/ ← 代码自动同步 (Webhook→git pull)
│ ├── secrets-vault/ ← 密钥保险库 (AES-256-GCM)
│ ├── portal/ ← Web Portal
│ ├── inference-agent/ ← 推理代理
@ -197,6 +199,8 @@ Phase 4 ─── 冰朔语言本体注入 ─── 池获得自我意识 →
| [📋 铸渊巡逻](https://guanghulab.com/git/bingshuo/guanghulab/actions) | 手动触发服务器巡检DeepSeek+千问分析) |
| [⚡ CVM开机](https://guanghulab.com/git/bingshuo/guanghulab/actions) | 按量计费CVM开机需配置腾讯云API密钥 |
> **代码自动同步**PR 合并到 main → Webhook 自动通知服务器 → Git Sync 拉取最新代码。无需手动操作。
>
> **关机流程**:开发完 → 点"安全关机检测" → 全绿 → 在腾讯云控制台关机 → 停止计费
---

161
server/git-sync/deploy.sh Normal file
View File

@ -0,0 +1,161 @@
#!/bin/bash
# ════════════════════════════════════════════════════════════════
# 光湖 · Git Sync 服务一键部署脚本
# 在新服务器 ZY-CVM-MAIN 上执行
# ════════════════════════════════════════════════════════════════
#
# 功能:
# 1. 部署 Git Sync 服务到 /data/guanghulab/server/git-sync/
# 2. 用 PM2 启动并设置开机自启
# 3. 配置 Nginx 反向代理
# 4. 在 Gitea 配置 Webhook
#
# 前提Forgejo 已运行PM2 已安装Nginx 已配置
# ════════════════════════════════════════════════════════════════
set -e
REPO_DIR="/data/guanghulab/repo"
SYNC_DIR="/data/guanghulab/server/git-sync"
TOKEN="3e16b79c427bb44b77c00b319aa641bde8f61d84"
GITEA_URL="http://127.0.0.1:3001"
echo "═══════════════════════════════════════"
echo "光湖 · Git Sync 部署"
echo "═══════════════════════════════════════"
# ─── 1. 同步代码到服务器 ───
echo ""
echo "▸ 步骤 1/5: 同步最新代码..."
cd "$REPO_DIR"
git fetch origin
git reset --hard origin/main
echo "✅ 代码已同步: $(git log --oneline -1)"
# ─── 2. 确保依赖已安装 ───
echo ""
echo "▸ 步骤 2/5: 安装依赖..."
if [ ! -d "$SYNC_DIR/node_modules" ]; then
cd "$SYNC_DIR"
npm install --production 2>/dev/null || true
echo "✅ 依赖已安装"
else
echo "✅ 依赖已存在,跳过"
fi
# ─── 3. 用 PM2 启动 Git Sync ───
echo ""
echo "▸ 步骤 3/5: 启动 Git Sync 服务..."
# 停止旧的(如果有)
pm2 delete guanghulab-git-sync 2>/dev/null || true
# 启动
cd "$SYNC_DIR"
ZY_REPO_TOKEN="$TOKEN" \
pm2 start server.js \
--name guanghulab-git-sync \
--env production
pm2 save
echo "✅ Git Sync 已启动 (端口 8082)"
# ─── 4. 配置 Nginx ───
echo ""
echo "▸ 步骤 4/5: 配置 Nginx 反向代理..."
NGINX_CONF="/etc/nginx/sites-enabled/guanghulab"
if grep -q "/sync/" "$NGINX_CONF" 2>/dev/null; then
echo "✅ Nginx 已有 /sync/ 路由,跳过"
else
# 在 /wake/ location 后面添加 /sync/ location
sed -i '/location \/wake\//,/}/a\
\
# ─── Git Sync 同步服务 (/sync/) ───\
location /sync/ {\
proxy_pass http://127.0.0.1:8082/;\
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;\
}' "$NGINX_CONF"
nginx -t && systemctl reload nginx
echo "✅ Nginx 已添加 /sync/ 路由"
fi
# ─── 5. 配置 Gitea Webhook ───
echo ""
echo "▸ 步骤 5/5: 配置 Gitea Webhook..."
# 生成 Webhook Secret
WEBHOOK_SECRET=$(openssl rand -hex 16)
echo " Webhook Secret: $WEBHOOK_SECRET"
# 检查是否已存在 Webhook
EXISTING=$(curl -s -H "Authorization: token $TOKEN" \
"$GITEA_URL/api/v1/repos/bingshuo/guanghulab/hooks" | \
python3 -c "
import sys, json
hooks = json.load(sys.stdin)
for h in hooks:
if h.get('config', {}).get('url', '').endswith('/sync/webhook'):
print(h['id'])
break
" 2>/dev/null || echo "")
if [ -n "$EXISTING" ]; then
# 更新现有 Webhook
curl -s -X PATCH -H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
"$GITEA_URL/api/v1/repos/bingshuo/guanghulab/hooks/$EXISTING" \
-d "{
\"config\": {
\"url\": \"http://127.0.0.1:8082/webhook\",
\"content_type\": \"json\",
\"secret\": \"$WEBHOOK_SECRET\"
},
\"events\": [\"push\"],
\"active\": true
}" > /dev/null
echo "✅ Webhook 已更新 (ID: $EXISTING)"
else
# 创建新 Webhook
curl -s -X POST -H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
"$GITEA_URL/api/v1/repos/bingshuo/guanghulab/hooks" \
-d "{
\"type\": \"gitea\",
\"config\": {
\"url\": \"http://127.0.0.1:8082/webhook\",
\"content_type\": \"json\",
\"secret\": \"$WEBHOOK_SECRET\"
},
\"events\": [\"push\"],
\"active\": true
}" > /dev/null
echo "✅ Webhook 已创建"
fi
# 将 Secret 写入 Git Sync 环境变量
pm2 set guanghulab-git-sync:GIT_SYNC_SECRET "$WEBHOOK_SECRET" 2>/dev/null || true
# 重启 Git Sync 让环境变量生效
pm2 restart guanghulab-git-sync --update-env \
--env ZY_REPO_TOKEN="$TOKEN" \
--env GIT_SYNC_SECRET="$WEBHOOK_SECRET"
echo ""
echo "═══════════════════════════════════════"
echo "✅ Git Sync 部署完成!"
echo ""
echo "工作方式:"
echo " · 你在 Gitea 合并 PR → 自动触发 Webhook"
echo " · Git Sync 收到通知 → 自动 git pull 到服务器"
echo " · 代码立即可用,无需手动操作"
echo ""
echo "测试命令:"
echo " curl http://127.0.0.1:8082/health"
echo " curl http://127.0.0.1:8082/status"
echo "═══════════════════════════════════════"

288
server/git-sync/server.js Normal file
View File

@ -0,0 +1,288 @@
/*
*
* 光湖 · Git 同步服务 (Git Sync)
* 监听 Gitea Webhook自动拉取最新代码到服务器
*
*
* 工作原理
* 1. Gitea 配置 Webhook push 事件 通知此服务
* 2. 本服务收到通知后执行 git pull 同步代码
* 3. 同步成功后重启相关 PM2 服务
*
* 安全设计
* - 验证 Webhook Secret防止伪造请求
* - 仅响应 push main 分支的事件
* - 同步操作以 root 权限运行确保 git 权限无问题
* - 同步锁防止并发执行
* - 同步失败自动重试最多3次
*
* 部署位置: /data/guanghulab/server/git-sync/
* PM2 进程名: guanghulab-git-sync
*
*/
"use strict";
const express = require("express");
const crypto = require("crypto");
const { execSync } = require("child_process");
const fs = require("fs");
const path = require("path");
const app = express();
app.use(express.json({ limit: "1mb" }));
// ─── 配置 ─────────────────────────────────────────────────────
const CONFIG = {
port: process.env.GIT_SYNC_PORT || 8082,
host: process.env.GIT_SYNC_HOST || "127.0.0.1",
// 仓库路径
repoDir: process.env.REPO_DIR || "/data/guanghulab/repo",
// Webhook Secret和 Gitea 里配置的一致)
webhookSecret: process.env.GIT_SYNC_SECRET || "",
// Git 认证信息(内嵌在 remote URL 中)
gitUser: process.env.GIT_USER || "bingshuo",
gitToken: process.env.ZY_REPO_TOKEN || "",
gitHost: process.env.GIT_HOST || "127.0.0.1:3001",
gitRepo: process.env.GIT_REPO || "bingshuo/guanghulab.git",
// 同步后要重启的 PM2 进程(逗号分隔)
restartServices: (process.env.RESTART_SERVICES || "").split(",").filter(Boolean),
// 重试配置
maxRetries: 3,
retryDelay: 5000, // 5秒
// 同步日志
syncLogPath: process.env.SYNC_LOG_PATH || "/data/guanghulab/.runtime/git-sync-log.json"
};
// ─── 同步锁 ─────────────────────────────────────────────────────
let isSyncing = false;
// ─── 同步日志 ──────────────────────────────────────────────────
let syncLog = [];
function loadSyncLog() {
try {
if (fs.existsSync(CONFIG.syncLogPath)) {
syncLog = JSON.parse(fs.readFileSync(CONFIG.syncLogPath, "utf8"));
}
} catch { syncLog = []; }
}
function saveSyncLog() {
try {
const dir = path.dirname(CONFIG.syncLogPath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
// 只保留最近50条
syncLog = syncLog.slice(-50);
fs.writeFileSync(CONFIG.syncLogPath, JSON.stringify(syncLog, null, 2));
} catch (e) {
console.error("[git-sync] 日志写入失败:", e.message);
}
}
function addLog(entry) {
syncLog.push({
...entry,
timestamp: new Date().toISOString()
});
saveSyncLog();
}
// ─── 验证 Webhook Secret ──────────────────────────────────────
function verifyWebhook(payload, signature) {
if (!CONFIG.webhookSecret) {
// 未配置 Secret 时跳过验证(仅限内网使用)
console.warn("[git-sync] ⚠️ Webhook Secret 未配置,跳过验证");
return true;
}
if (!signature) return false;
const expected = crypto
.createHmac("sha256", CONFIG.webhookSecret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(`sha256=${expected}`),
Buffer.from(signature)
);
}
// ─── 执行 Git 同步 ────────────────────────────────────────────
function doSync(commitSha, committer, message) {
if (isSyncing) {
console.log("[git-sync] 同步进行中,跳过");
return { skipped: true };
}
isSyncing = true;
try {
// 确保 remote URL 包含认证信息
const authUrl = `http://${CONFIG.gitUser}:${CONFIG.gitToken}@${CONFIG.gitHost}/${CONFIG.gitRepo}`;
execSync(`cd ${CONFIG.repoDir} && git remote set-url origin ${authUrl}`, {
encoding: "utf8",
timeout: 10000
});
// 执行 git pull
const output = execSync(
`cd ${CONFIG.repoDir} && git fetch origin && git reset --hard origin/main && git log --oneline -1`,
{ encoding: "utf8", timeout: 30000 }
);
console.log(`[git-sync] ✅ 同步成功: ${output.trim()}`);
// 同步后重启相关服务
if (CONFIG.restartServices.length > 0) {
for (const svc of CONFIG.restartServices) {
try {
execSync(`pm2 restart ${svc}`, { encoding: "utf8", timeout: 15000 });
console.log(`[git-sync] 🔄 已重启: ${svc}`);
} catch (e) {
console.error(`[git-sync] ⚠️ 重启失败 ${svc}: ${e.message}`);
}
}
}
addLog({
status: "success",
commit: commitSha || output.trim().split(" ")[0],
committer: committer || "unknown",
message: message || output.trim(),
output: output.trim()
});
return { success: true, output: output.trim() };
} catch (e) {
console.error(`[git-sync] ❌ 同步失败: ${e.message}`);
addLog({
status: "failed",
commit: commitSha,
committer: committer || "unknown",
message: message || "",
error: e.message
});
return { success: false, error: e.message };
} finally {
isSyncing = false;
}
}
// ─── 带重试的同步 ──────────────────────────────────────────────
async function syncWithRetry(commitSha, committer, message) {
for (let attempt = 1; attempt <= CONFIG.maxRetries; attempt++) {
const result = doSync(commitSha, committer, message);
if (result.success || result.skipped) return result;
if (attempt < CONFIG.maxRetries) {
console.log(`[git-sync] 重试 ${attempt}/${CONFIG.maxRetries}...`);
await new Promise(r => setTimeout(r, CONFIG.retryDelay));
}
}
return { success: false, error: "重试次数已用完" };
}
// ─── API 路由 ──────────────────────────────────────────────────
// Webhook 接收端点
app.post("/webhook", async (req, res) => {
const signature = req.headers["x-gitea-signature"] || req.headers["x-hub-signature-256"] || "";
const payload = JSON.stringify(req.body);
// 验证签名
if (!verifyWebhook(payload, signature)) {
console.warn("[git-sync] ❌ Webhook 签名验证失败");
return res.status(403).json({ error: "签名验证失败" });
}
const body = req.body;
const ref = body.ref || "";
const commitSha = body.after || body.head_commit?.id || "";
const committer = body.pusher?.login || body.sender?.login || "unknown";
const message = body.head_commit?.message || body.commits?.[0]?.message || "";
// 仅响应 push 到 main 分支
if (!ref.endsWith("/main")) {
console.log(`[git-sync] 忽略非 main 分支推送: ${ref}`);
return res.json({ ignored: true, ref });
}
console.log(`[git-sync] 📥 收到 push 事件: ${commitSha.substring(0, 8)} by ${committer}`);
// 异步执行同步(不阻塞 Webhook 响应)
syncWithRetry(commitSha, committer, message)
.then(result => {
if (result.success) {
console.log(`[git-sync] ✅ 同步完成: ${commitSha.substring(0, 8)}`);
} else {
console.error(`[git-sync] ❌ 同步最终失败: ${result.error}`);
}
});
// 立即返回 200Gitea 不需要等待同步完成
res.json({ received: true, syncing: true, commit: commitSha.substring(0, 8) });
});
// 手动触发同步
app.post("/sync", async (req, res) => {
if (isSyncing) {
return res.json({ syncing: true, message: "同步进行中" });
}
const result = await syncWithRetry("", "manual", "手动触发");
res.json(result);
});
// 同步状态查询
app.get("/status", (req, res) => {
const lastSync = syncLog.length > 0 ? syncLog[syncLog.length - 1] : null;
// 读取当前仓库状态
let repoStatus = null;
try {
const log = execSync(`cd ${CONFIG.repoDir} && git log --oneline -1 && git status --short`, {
encoding: "utf8", timeout: 10000
});
repoStatus = log.trim();
} catch {
repoStatus = "无法读取";
}
res.json({
service: "git-sync",
status: isSyncing ? "syncing" : "idle",
lastSync,
repoStatus,
totalSyncs: syncLog.length,
failedSyncs: syncLog.filter(l => l.status === "failed").length
});
});
// 健康检查
app.get("/health", (req, res) => {
res.json({ status: "ok", uptime: process.uptime() });
});
// ─── 启动 ─────────────────────────────────────────────────────
loadSyncLog();
app.listen(CONFIG.port, CONFIG.host, () => {
console.log(`[git-sync] Git 同步服务启动: http://${CONFIG.host}:${CONFIG.port}`);
console.log(`[git-sync] 仓库路径: ${CONFIG.repoDir}`);
console.log(`[git-sync] Webhook 端点: POST /webhook`);
console.log(`[git-sync] 铸渊 · ICE-GL-ZY001 · TCS-0002∞`);
});

View File

@ -0,0 +1,93 @@
# ═══════════════════════════════════════════════════════════
# 光湖主控域名 · guanghulab.com Nginx 配置
# ═══════════════════════════════════════════════════════════
#
# 服务器: ZY-CVM-MAIN · 42.193.161.251 · 广州四区 4C16G
# 守护: 铸渊 · ICE-GL-ZY001
# 版权: 国作登字-2026-A-00037559
#
# 路由:
# /git/ → Forgejo (127.0.0.1:3001)
# /wake/ → Wake Gate 唤醒门 (127.0.0.1:8081)
# /sync/ → Git Sync 同步服务 (127.0.0.1:8082)
# / → 仓库首页 (Forgejo 处理)
#
# SSL: Let's Encrypt via certbot --nginx
# ═══════════════════════════════════════════════════════════
# HTTP → HTTPS 重定向
server {
listen 80;
server_name guanghulab.com www.guanghulab.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name guanghulab.com www.guanghulab.com;
# ─── SSL (certbot 自动管理) ───
# certbot --nginx 会自动填充以下配置
# ssl_certificate /etc/letsencrypt/live/guanghulab.com/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/guanghulab.com/privkey.pem;
# ─── 安全头 ───
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Server-Identity "ZY-CVM-MAIN" always;
# ─── Forgejo 代码仓库 (/git/) ───
location /git/ {
proxy_pass http://127.0.0.1:3001/;
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;
# 大文件上传支持
client_max_body_size 100M;
# WebSocket (Forgejo UI)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# ─── Forgejo Actions gRPC (Runner 直连 127.0.0.1:3001) ───
# Nginx 不代理 gRPCRunner 直接连接 Forgejo
# ─── Wake Gate 唤醒门 (/wake/) ───
location /wake/ {
proxy_pass http://127.0.0.1:8081/;
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;
}
# ─── Git Sync 同步服务 (/sync/) ───
location /sync/ {
proxy_pass http://127.0.0.1:8082/;
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;
# 仅允许内网和本地访问 Webhook
# allow 127.0.0.1;
# allow 10.0.0.0/8;
# deny all;
}
# ─── 健康探针 ───
location = /health {
access_log off;
return 200 '{"status":"ok","server":"ZY-CVM-MAIN"}';
add_header Content-Type application/json;
}
# ─── 访问日志 ───
access_log /var/log/nginx/guanghulab-access.log;
error_log /var/log/nginx/guanghulab-error.log;
}