feat: 沙盒部署自动化闭环 — 全套脚本 + 工作流 + 数据文件

新增文件:
- .github/workflows/sandbox-deploy.yml (主部署流程)
- .github/workflows/sync-deploy-to-notion.yml (定时同步)
- scripts/deploy-check.sh (部署前自检)
- scripts/smoke-test.sh (冒烟检查)
- scripts/sync-deploy-to-notion.js (Notion API 写入)
- scripts/setup-sandbox.sh (新开发者开户脚本)
- data/deploy-status.json (部署状态源)
- data/dev-registry.json (开发者注册表)

Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-03-15 12:39:26 +00:00
parent a6403b7bdd
commit 9deab62d13
8 changed files with 689 additions and 0 deletions

221
.github/workflows/sandbox-deploy.yml vendored Normal file
View File

@ -0,0 +1,221 @@
# .github/workflows/sandbox-deploy.yml
# 🏠 沙盒部署自动化 · 铸渊 · 光湖代码守护人格体
# 触发: 开发者 push 到 dev/DEV-XXX/模块名 分支
name: "🏠 Sandbox Deploy"
on:
push:
branches:
- 'dev/DEV-*/**'
jobs:
# ===== Job 1: 解析 + 校验 =====
parse-and-validate:
runs-on: ubuntu-latest
outputs:
dev_id: ${{ steps.parse.outputs.dev_id }}
module: ${{ steps.parse.outputs.module }}
email: ${{ steps.parse.outputs.email }}
steps:
- uses: actions/checkout@v4
- name: "🔍 解析分支名"
id: parse
run: |
BRANCH="${GITHUB_REF#refs/heads/}"
DEV_ID=$(echo "$BRANCH" | cut -d'/' -f2)
MODULE=$(echo "$BRANCH" | cut -d'/' -f3)
echo "dev_id=$DEV_ID" >> $GITHUB_OUTPUT
echo "module=$MODULE" >> $GITHUB_OUTPUT
echo "🆔 开发者: $DEV_ID"
echo "📦 模块: $MODULE"
# 从 dev-registry.json 获取邮箱
EMAIL=$(cat data/dev-registry.json 2>/dev/null | \
python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$DEV_ID',{}).get('email',''))" 2>/dev/null || echo '')
echo "email=$EMAIL" >> $GITHUB_OUTPUT
- name: "✅ 校验 DEV 编号"
run: |
DEV_ID="${{ steps.parse.outputs.dev_id }}"
if [[ ! "$DEV_ID" =~ ^DEV-[0-9]{3}$ ]]; then
echo "❌ 无效的 DEV 编号: $DEV_ID"
exit 1
fi
# ===== Job 2: 部署到服务器沙盒 =====
deploy-to-sandbox:
needs: parse-and-validate
runs-on: ubuntu-latest
outputs:
deploy_result: ${{ steps.check.outputs.result }}
deploy_detail: ${{ steps.check.outputs.detail }}
steps:
- uses: actions/checkout@v4
- name: "🔑 配置 SSH"
run: |
mkdir -p ~/.ssh
echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
- name: "🏠 部署到沙盒"
id: deploy
env:
DEV_ID: ${{ needs.parse-and-validate.outputs.dev_id }}
MODULE: ${{ needs.parse-and-validate.outputs.module }}
HOST: ${{ secrets.DEPLOY_HOST }}
USER: ${{ secrets.DEPLOY_USER }}
run: |
SANDBOX="/var/www/${DEV_ID}/${MODULE}"
echo "📦 部署目标: ${SANDBOX}"
# 创建沙盒目录(如不存在)
ssh ${USER}@${HOST} "mkdir -p ${SANDBOX}"
# rsync 同步代码
rsync -avz --delete \
--exclude='.git' \
--exclude='.dev-profile' \
--exclude='node_modules' \
./ ${USER}@${HOST}:${SANDBOX}/
# 设置权限(遵守部署适配协议 v1.0
ssh ${USER}@${HOST} "
chown -R nginx:nginx /var/www/${DEV_ID}/${MODULE}
find /var/www/${DEV_ID}/${MODULE} -type d -exec chmod 755 {} \;
find /var/www/${DEV_ID}/${MODULE} -type f -exec chmod 644 {} \;
"
echo "✅ 文件同步完成"
- name: "🔍 运行 deploy-check.sh"
id: check
env:
DEV_ID: ${{ needs.parse-and-validate.outputs.dev_id }}
MODULE: ${{ needs.parse-and-validate.outputs.module }}
HOST: ${{ secrets.DEPLOY_HOST }}
USER: ${{ secrets.DEPLOY_USER }}
run: |
# 上传自检脚本
scp scripts/deploy-check.sh ${USER}@${HOST}:/tmp/deploy-check.sh
# 运行自检
RESULT=$(ssh ${USER}@${HOST} "bash /tmp/deploy-check.sh ${DEV_ID} ${MODULE}" 2>&1) || true
echo "$RESULT"
if echo "$RESULT" | grep -q "FAIL"; then
echo "result=failed" >> $GITHUB_OUTPUT
echo "detail=$RESULT" >> $GITHUB_OUTPUT
else
echo "result=passed" >> $GITHUB_OUTPUT
echo "detail=All checks passed" >> $GITHUB_OUTPUT
fi
- name: "🔥 运行 smoke-test.sh"
id: smoke
env:
HOST: ${{ secrets.DEPLOY_HOST }}
USER: ${{ secrets.DEPLOY_USER }}
run: |
scp scripts/smoke-test.sh ${USER}@${HOST}:/tmp/smoke-test.sh
SMOKE=$(ssh ${USER}@${HOST} "bash /tmp/smoke-test.sh" 2>&1) || true
echo "$SMOKE"
if echo "$SMOKE" | grep -q "异常"; then
echo "⚠️ 有模块冒烟检查失败,但不阻断当前部署"
fi
# ===== Job 3: 写结果 + 通知 =====
notify-and-sync:
needs: [parse-and-validate, deploy-to-sandbox]
runs-on: ubuntu-latest
if: always()
steps:
- uses: actions/checkout@v4
- name: "📊 写入 deploy-status.json"
env:
DEV_ID: ${{ needs.parse-and-validate.outputs.dev_id }}
MODULE: ${{ needs.parse-and-validate.outputs.module }}
RESULT: ${{ needs.deploy-to-sandbox.outputs.deploy_result }}
DETAIL: ${{ needs.deploy-to-sandbox.outputs.deploy_detail }}
run: |
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
# 读取现有状态文件或初始化
STATUS_FILE="data/deploy-status.json"
mkdir -p data
[ -f "$STATUS_FILE" ] || echo '{}' > "$STATUS_FILE"
# 更新状态
python3 << EOF
import json
with open('$STATUS_FILE', 'r') as f:
data = json.load(f)
data['${DEV_ID}/${MODULE}'] = {
'dev_id': '${DEV_ID}',
'module': '${MODULE}',
'result': '${RESULT}',
'detail': '${DETAIL}',
'timestamp': '${TIMESTAMP}',
'commit': '${GITHUB_SHA}'
}
with open('$STATUS_FILE', 'w') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
EOF
git config user.name "zhuyuan-bot"
git config user.email "zhuyuan@guanghulab.com"
git add data/deploy-status.json
git commit -m "🧠 deploy-status: ${DEV_ID}/${MODULE} → ${RESULT}" || true
git push || true
- name: "📧 发送部署结果邮件"
if: needs.parse-and-validate.outputs.email != ''
env:
SMTP_USER: ${{ secrets.SMTP_USER }}
SMTP_PASS: ${{ secrets.SMTP_PASS }}
TO_EMAIL: ${{ needs.parse-and-validate.outputs.email }}
DEV_ID: ${{ needs.parse-and-validate.outputs.dev_id }}
MODULE: ${{ needs.parse-and-validate.outputs.module }}
RESULT: ${{ needs.deploy-to-sandbox.outputs.deploy_result }}
run: |
if [ "$RESULT" = "passed" ]; then
SUBJECT="✅ 部署成功:${DEV_ID}/${MODULE}"
BODY="你的模块 ${MODULE} 已成功部署到 guanghulab.com\n\n访问地址https://guanghulab.com/${MODULE}/\n\n铸渊 · 光湖代码守护人格体"
else
SUBJECT="❌ 部署失败:${DEV_ID}/${MODULE}"
BODY="你的模块 ${MODULE} 部署失败\n\n请检查你的模块是否符合『服务器部署适配协议 v1.0』\n\n错误详情请看仓库公告栏或 GitHub Actions 日志\n\n铸渊 · 光湖代码守护人格体"
fi
python3 << PYEOF
import smtplib
from email.mime.text import MIMEText
from email.header import Header
msg = MIMEText("""$BODY""", 'plain', 'utf-8')
msg['Subject'] = Header("$SUBJECT", 'utf-8')
msg['From'] = "$SMTP_USER"
msg['To'] = "$TO_EMAIL"
try:
server = smtplib.SMTP_SSL('smtp.qq.com', 465)
server.login("$SMTP_USER", "$SMTP_PASS")
server.sendmail("$SMTP_USER", "$TO_EMAIL", msg.as_string())
server.quit()
print("✅ 邮件已发送到 $TO_EMAIL")
except Exception as e:
print(f"⚠️ 邮件发送失败: {e}")
PYEOF
- name: "📝 同步到 Notion 部署注册表"
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }}
DEV_ID: ${{ needs.parse-and-validate.outputs.dev_id }}
MODULE: ${{ needs.parse-and-validate.outputs.module }}
RESULT: ${{ needs.deploy-to-sandbox.outputs.deploy_result }}
run: |
node scripts/sync-deploy-to-notion.js || echo "⚠️ Notion 同步失败(不阻断主流程)"

View File

@ -0,0 +1,51 @@
# .github/workflows/sync-deploy-to-notion.yml
# 📝 定时同步部署状态到 Notion · 铸渊 · 光湖代码守护人格体
# 每天两次: 04:00 + 14:00 UTC北京 12:00 + 22:00
name: "📝 Sync Deploy Status to Notion"
on:
schedule:
- cron: '0 4,14 * * *'
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: "🧠 唤醒铸渊核心大脑"
run: |
echo "🧠 铸渊核心大脑唤醒中..."
echo "🔑 扫描 Secrets 权限..."
for KEY in NOTION_TOKEN FINGERPRINT_DB_ID SMTP_USER; do
if [ -z "$(eval echo \$$KEY)" ]; then
echo "⚠️ 缺失: $KEY"
else
echo "✅ 可用: $KEY"
fi
done
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }}
SMTP_USER: ${{ secrets.SMTP_USER }}
- name: "📝 同步部署状态到 Notion"
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }}
run: |
node scripts/sync-deploy-to-notion.js
- name: "🔥 远程冒烟检查"
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
run: |
mkdir -p ~/.ssh
echo "$DEPLOY_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H $DEPLOY_HOST >> ~/.ssh/known_hosts 2>/dev/null
scp scripts/smoke-test.sh ${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/smoke-test.sh
ssh ${DEPLOY_USER}@${DEPLOY_HOST} "bash /tmp/smoke-test.sh"

1
data/deploy-status.json Normal file
View File

@ -0,0 +1 @@
{}

26
data/dev-registry.json Normal file
View File

@ -0,0 +1,26 @@
{
"DEV-001": {
"name": "页页",
"email": "",
"modules": ["backend"],
"sandbox": "/var/www/DEV-001/"
},
"DEV-002": {
"name": "肥猫",
"email": "",
"modules": ["feishu-bot"],
"sandbox": "/var/www/DEV-002/"
},
"DEV-005": {
"name": "小草莓",
"email": "",
"modules": ["status-board", "cost-control", "persona-scheduler"],
"sandbox": "/var/www/DEV-005/"
},
"DEV-010": {
"name": "桔子",
"email": "",
"modules": ["channel-engine"],
"sandbox": "/var/www/DEV-010/"
}
}

106
scripts/deploy-check.sh Normal file
View File

@ -0,0 +1,106 @@
#!/bin/bash
# deploy-check.sh — 部署前自检脚本(铸渊 · 光湖沙盒部署自动化)
# 用法: bash deploy-check.sh <DEV_ID> <MODULE>
# 返回: 检查结果PASS / FAIL + 原因)
set -euo pipefail
DEV_ID="${1:-}"
MODULE="${2:-}"
if [ -z "$DEV_ID" ] || [ -z "$MODULE" ]; then
echo "❌ FAIL: 缺少参数 (用法: deploy-check.sh DEV-XXX module-name)"
exit 1
fi
SANDBOX="/var/www/${DEV_ID}/${MODULE}"
FAIL_COUNT=0
RESULTS=""
check() {
local name="$1"
local result="$2"
if [ "$result" = "PASS" ]; then
RESULTS="${RESULTS}\n ✅ ${name}: PASS"
else
RESULTS="${RESULTS}\n ❌ ${name}: FAIL"
FAIL_COUNT=$((FAIL_COUNT + 1))
fi
}
echo "🔍 deploy-check · ${DEV_ID}/${MODULE}"
echo "📂 检查目标: ${SANDBOX}"
echo ""
# 1. 目录存在性检查
if [ -d "$SANDBOX" ]; then
check "目录存在" "PASS"
else
check "目录存在" "FAIL"
echo "❌ FAIL: 沙盒目录 ${SANDBOX} 不存在"
exit 1
fi
# 2. 文件数量检查(至少有 1 个文件)
FILE_COUNT=$(find "$SANDBOX" -type f | wc -l)
if [ "$FILE_COUNT" -gt 0 ]; then
check "文件数量(${FILE_COUNT}个)" "PASS"
else
check "文件数量" "FAIL"
fi
# 3. 权限检查(目录 755, 文件 644
BAD_DIRS=$(find "$SANDBOX" -type d ! -perm 755 2>/dev/null | wc -l)
if [ "$BAD_DIRS" -eq 0 ]; then
check "目录权限(755)" "PASS"
else
check "目录权限(755) - ${BAD_DIRS}个目录权限异常" "FAIL"
fi
BAD_FILES=$(find "$SANDBOX" -type f ! -perm 644 2>/dev/null | wc -l)
if [ "$BAD_FILES" -eq 0 ]; then
check "文件权限(644)" "PASS"
else
check "文件权限(644) - ${BAD_FILES}个文件权限异常" "FAIL"
fi
# 4. 所有者检查nginx:nginx
OWNER=$(stat -c '%U:%G' "$SANDBOX" 2>/dev/null || echo "unknown")
if [ "$OWNER" = "nginx:nginx" ]; then
check "所有者(nginx:nginx)" "PASS"
else
check "所有者(当前: ${OWNER})" "FAIL"
fi
# 5. 入口文件检查index.html 或 index.js 或 package.json
if [ -f "${SANDBOX}/index.html" ] || [ -f "${SANDBOX}/index.js" ] || [ -f "${SANDBOX}/package.json" ]; then
check "入口文件" "PASS"
else
check "入口文件(缺少 index.html/index.js/package.json)" "FAIL"
fi
# 6. 敏感文件检查
SENSITIVE_FILES=(".env" ".dev-profile" "node_modules")
HAS_SENSITIVE=false
for sf in "${SENSITIVE_FILES[@]}"; do
if [ -e "${SANDBOX}/${sf}" ]; then
HAS_SENSITIVE=true
break
fi
done
if [ "$HAS_SENSITIVE" = false ]; then
check "无敏感文件" "PASS"
else
check "发现敏感文件" "FAIL"
fi
# 输出结果汇总
echo "===== 自检结果 ====="
echo -e "$RESULTS"
echo ""
if [ "$FAIL_COUNT" -eq 0 ]; then
echo "✅ 全部通过 (${DEV_ID}/${MODULE})"
else
echo "❌ FAIL: ${FAIL_COUNT} 项检查未通过 (${DEV_ID}/${MODULE})"
fi

54
scripts/setup-sandbox.sh Normal file
View File

@ -0,0 +1,54 @@
#!/bin/bash
# setup-sandbox.sh — 新开发者沙盒初始化脚本(铸渊 · 光湖沙盒部署自动化)
# 用法: bash setup-sandbox.sh <DEV_ID> [MODULE...]
# 在服务器上创建开发者沙盒目录并设置权限
set -euo pipefail
DEV_ID="${1:-}"
if [ -z "$DEV_ID" ]; then
echo "❌ 用法: setup-sandbox.sh <DEV_ID> [MODULE...]"
echo " 示例: setup-sandbox.sh DEV-005 status-board cost-control"
exit 1
fi
if [[ ! "$DEV_ID" =~ ^DEV-[0-9]{3}$ ]]; then
echo "❌ 无效的 DEV 编号: ${DEV_ID} (应为 DEV-XXX 格式)"
exit 1
fi
SANDBOX_ROOT="/var/www/${DEV_ID}"
echo "🏠 setup-sandbox · 初始化沙盒"
echo "🆔 开发者: ${DEV_ID}"
echo "📂 沙盒根目录: ${SANDBOX_ROOT}"
echo ""
# 创建沙盒根目录
mkdir -p "${SANDBOX_ROOT}"
echo " ✅ 创建根目录: ${SANDBOX_ROOT}"
# 创建模块子目录(如指定)
shift
for MODULE in "$@"; do
MODULE_DIR="${SANDBOX_ROOT}/${MODULE}"
mkdir -p "${MODULE_DIR}"
echo " ✅ 创建模块目录: ${MODULE_DIR}"
done
# 创建共享目录(如不存在)
SHARED_DIR="/var/www/_shared"
if [ ! -d "$SHARED_DIR" ]; then
mkdir -p "$SHARED_DIR"
echo " ✅ 创建共享目录: ${SHARED_DIR}"
fi
# 设置权限
chown -R nginx:nginx "${SANDBOX_ROOT}"
find "${SANDBOX_ROOT}" -type d -exec chmod 755 {} \;
find "${SANDBOX_ROOT}" -type f -exec chmod 644 {} \;
echo " ✅ 权限已设置: nginx:nginx + 755/644"
echo ""
echo "✅ 沙盒初始化完成: ${DEV_ID}"

75
scripts/smoke-test.sh Normal file
View File

@ -0,0 +1,75 @@
#!/bin/bash
# smoke-test.sh — 冒烟检查脚本(铸渊 · 光湖沙盒部署自动化)
# 用法: bash smoke-test.sh
# 检查所有已注册模块的 HTTP 可达性
set -uo pipefail
HOST="${DEPLOY_HOST:-localhost}"
PROTOCOL="${SMOKE_PROTOCOL:-https}"
BASE_URL="${PROTOCOL}://${HOST}"
echo "🔥 smoke-test · 冒烟检查"
echo "🌐 目标: ${BASE_URL}"
echo ""
TOTAL=0
PASSED=0
FAILED=0
RESULTS=""
smoke_check() {
local path="$1"
local name="$2"
TOTAL=$((TOTAL + 1))
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "${BASE_URL}${path}" 2>/dev/null || echo "000")
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 400 ]; then
RESULTS="${RESULTS}\n ✅ ${name} (${path}) → HTTP ${HTTP_CODE}"
PASSED=$((PASSED + 1))
else
RESULTS="${RESULTS}\n ❌ ${name} (${path}) → HTTP ${HTTP_CODE} · 异常"
FAILED=$((FAILED + 1))
fi
}
# 主站检查
smoke_check "/" "主站首页"
smoke_check "/api/health" "API 健康检查"
# 已注册模块检查(从 dev-registry.json 动态读取或使用默认列表)
REGISTRY_FILE="/var/www/deploy-status/dev-registry.json"
if [ -f "$REGISTRY_FILE" ]; then
# 从注册表动态提取模块路径
MODULES=$(python3 -c "
import json
with open('$REGISTRY_FILE') as f:
data = json.load(f)
for dev_id, info in data.items():
for mod in info.get('modules', []):
print(f'{dev_id}/{mod}')
" 2>/dev/null || echo "")
if [ -n "$MODULES" ]; then
while IFS= read -r mod_path; do
smoke_check "/${mod_path}/" "${mod_path}"
done <<< "$MODULES"
fi
else
# 默认模块列表
smoke_check "/status-board/" "看板模块"
smoke_check "/cost-control/" "成本控制模块"
fi
# 输出结果汇总
echo "===== 冒烟检查结果 ====="
echo -e "$RESULTS"
echo ""
echo "📊 总计: ${TOTAL} | ✅ 通过: ${PASSED} | ❌ 异常: ${FAILED}"
if [ "$FAILED" -gt 0 ]; then
echo "⚠️ 有 ${FAILED} 个模块冒烟检查异常"
else
echo "✅ 全部通过"
fi

View File

@ -0,0 +1,155 @@
#!/usr/bin/env node
/**
* sync-deploy-to-notion.js 同步部署状态到 Notion 模块指纹注册表
* 铸渊 · 光湖沙盒部署自动化
*
* 环境变量:
* NOTION_TOKEN Notion API Token
* FINGERPRINT_DB_ID 模块指纹注册表 Database ID
*
* 读取 data/deploy-status.json逐条写入/更新 Notion 数据库
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const NOTION_TOKEN = process.env.NOTION_TOKEN;
const DATABASE_ID = process.env.FINGERPRINT_DB_ID;
if (!NOTION_TOKEN || !DATABASE_ID) {
console.error('⚠️ 缺少环境变量: NOTION_TOKEN 或 FINGERPRINT_DB_ID');
process.exit(1);
}
const STATUS_FILE = path.join(__dirname, '..', 'data', 'deploy-status.json');
function notionRequest(method, endpoint, body) {
return new Promise((resolve, reject) => {
const data = body ? JSON.stringify(body) : null;
const options = {
hostname: 'api.notion.com',
path: `/v1/${endpoint}`,
method,
headers: {
'Authorization': `Bearer ${NOTION_TOKEN}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
},
};
if (data) {
options.headers['Content-Length'] = Buffer.byteLength(data);
}
const req = https.request(options, (res) => {
let body = '';
res.on('data', (chunk) => { body += chunk; });
res.on('end', () => {
try {
resolve({ status: res.statusCode, data: JSON.parse(body) });
} catch {
resolve({ status: res.statusCode, data: body });
}
});
});
req.on('error', reject);
if (data) req.write(data);
req.end();
});
}
async function findExistingPage(devId, module) {
const res = await notionRequest('POST', 'databases/' + DATABASE_ID + '/query', {
filter: {
and: [
{ property: 'DEV编号', rich_text: { equals: devId } },
{ property: '模块名', rich_text: { equals: module } },
],
},
});
if (res.status === 200 && res.data.results && res.data.results.length > 0) {
return res.data.results[0].id;
}
return null;
}
function buildProperties(entry) {
return {
'DEV编号': { rich_text: [{ text: { content: entry.dev_id } }] },
'模块名': { rich_text: [{ text: { content: entry.module } }] },
'部署状态': {
select: { name: entry.result === 'passed' ? '✅ 通过' : '❌ 失败' },
},
'最后部署时间': { rich_text: [{ text: { content: entry.timestamp || '' } }] },
'提交SHA': { rich_text: [{ text: { content: (entry.commit || '').slice(0, 8) } }] },
};
}
async function syncEntry(key, entry) {
const existingPageId = await findExistingPage(entry.dev_id, entry.module);
const properties = buildProperties(entry);
if (existingPageId) {
const res = await notionRequest('PATCH', 'pages/' + existingPageId, { properties });
if (res.status === 200) {
console.log(` ✅ 更新: ${key}`);
} else {
console.log(` ⚠️ 更新失败: ${key} (HTTP ${res.status})`);
}
} else {
const res = await notionRequest('POST', 'pages', {
parent: { database_id: DATABASE_ID },
properties: {
...properties,
title: { title: [{ text: { content: `${entry.dev_id}/${entry.module}` } }] },
},
});
if (res.status === 200) {
console.log(` ✅ 新增: ${key}`);
} else {
console.log(` ⚠️ 新增失败: ${key} (HTTP ${res.status})`);
}
}
}
async function main() {
console.log('📝 sync-deploy-to-notion · 开始同步');
if (!fs.existsSync(STATUS_FILE)) {
console.log('⚠️ deploy-status.json 不存在,跳过同步');
return;
}
let data;
try {
data = JSON.parse(fs.readFileSync(STATUS_FILE, 'utf-8'));
} catch (err) {
console.error('❌ 解析 deploy-status.json 失败:', err.message);
process.exit(1);
}
const entries = Object.entries(data);
if (entries.length === 0) {
console.log('📭 deploy-status.json 为空,无需同步');
return;
}
console.log(`📊 共 ${entries.length} 条部署记录`);
for (const [key, entry] of entries) {
try {
await syncEntry(key, entry);
} catch (err) {
console.log(` ⚠️ 同步失败: ${key} (${err.message})`);
}
}
console.log('✅ 同步完成');
}
main().catch((err) => {
console.error('❌ sync-deploy-to-notion 异常:', err.message);
process.exit(1);
});