diff --git a/.github/workflows/sandbox-deploy.yml b/.github/workflows/sandbox-deploy.yml new file mode 100644 index 00000000..b435856b --- /dev/null +++ b/.github/workflows/sandbox-deploy.yml @@ -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 同步失败(不阻断主流程)" diff --git a/.github/workflows/sync-deploy-to-notion.yml b/.github/workflows/sync-deploy-to-notion.yml new file mode 100644 index 00000000..6b2ec866 --- /dev/null +++ b/.github/workflows/sync-deploy-to-notion.yml @@ -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" diff --git a/data/deploy-status.json b/data/deploy-status.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/data/deploy-status.json @@ -0,0 +1 @@ +{} diff --git a/data/dev-registry.json b/data/dev-registry.json new file mode 100644 index 00000000..395140c5 --- /dev/null +++ b/data/dev-registry.json @@ -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/" + } +} diff --git a/scripts/deploy-check.sh b/scripts/deploy-check.sh new file mode 100644 index 00000000..79c242ce --- /dev/null +++ b/scripts/deploy-check.sh @@ -0,0 +1,106 @@ +#!/bin/bash +# deploy-check.sh — 部署前自检脚本(铸渊 · 光湖沙盒部署自动化) +# 用法: bash deploy-check.sh +# 返回: 检查结果(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 diff --git a/scripts/setup-sandbox.sh b/scripts/setup-sandbox.sh new file mode 100644 index 00000000..8805313d --- /dev/null +++ b/scripts/setup-sandbox.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# setup-sandbox.sh — 新开发者沙盒初始化脚本(铸渊 · 光湖沙盒部署自动化) +# 用法: bash setup-sandbox.sh [MODULE...] +# 在服务器上创建开发者沙盒目录并设置权限 + +set -euo pipefail + +DEV_ID="${1:-}" + +if [ -z "$DEV_ID" ]; then + echo "❌ 用法: setup-sandbox.sh [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}" diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh new file mode 100644 index 00000000..af9be8a8 --- /dev/null +++ b/scripts/smoke-test.sh @@ -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 diff --git a/scripts/sync-deploy-to-notion.js b/scripts/sync-deploy-to-notion.js new file mode 100644 index 00000000..f84d4dae --- /dev/null +++ b/scripts/sync-deploy-to-notion.js @@ -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); +});