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:
parent
a6403b7bdd
commit
9deab62d13
|
|
@ -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 同步失败(不阻断主流程)"
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
||||||
|
|
@ -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/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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}"
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue