diff --git a/.gitea/docs/PULL_DEPLOY.md b/.gitea/docs/PULL_DEPLOY.md new file mode 100644 index 0000000..b422913 --- /dev/null +++ b/.gitea/docs/PULL_DEPLOY.md @@ -0,0 +1,68 @@ +# 拉取式部署配置(一次性) + +> 安全原则:CI 只构建、不部署;任何 AI 与流水线都不允许 SSH 到服务器执行命令。 +> 部署由服务器本地的 watcher 监听 CI 产物目录自动完成。本文档的所有命令由**管理员本人**在服务器上执行一次。 + +## 工作原理 + +``` +push main → Gitea Actions 构建 → 产物写入 /ci-artifacts(runner 与宿主同机) + ↓ +服务器本地 systemd timer 每分钟运行 deploy-watch.sh → 发现新 sha256 → 执行 deploy.sh + ↓ + 目录级原子切换 → docker restart → HTTP 健康检查 → 失败自动回滚 +``` + +## 一次性安装(管理员手工执行) + +```bash +# 1. 放置脚本 +sudo mkdir -p /opt/lunar/scripts +sudo cp deploy.sh deploy-watch.sh /opt/lunar/scripts/ # 从仓库 .gitea/scripts/ 复制 +sudo chmod +x /opt/lunar/scripts/*.sh + +# 2. systemd timer(每分钟轮询) +sudo tee /etc/systemd/system/lunar-deploy-watch.service > /dev/null <<'EOF' +[Unit] +Description=Lunar pull-based deploy watcher +[Service] +Type=oneshot +ExecStart=/bin/bash /opt/lunar/scripts/deploy-watch.sh +EOF + +sudo tee /etc/systemd/system/lunar-deploy-watch.timer > /dev/null <<'EOF' +[Unit] +Description=Run lunar deploy watcher every minute +[Timer] +OnBootSec=1min +OnUnitActiveSec=1min +[Install] +WantedBy=timers.target +EOF + +sudo systemctl daemon-reload +sudo systemctl enable --now lunar-deploy-watch.timer +``` + +## 验证 + +```bash +systemctl list-timers | grep lunar +tail -f /opt/lunar/deploy-watch.log +curl -s http://localhost:8080/api/version +``` + +## 生产容器必备环境变量 + +在 1Panel 容器配置中设置(切勿写入仓库): + +- `DB_HOST` / `DB_PORT` / `DB_USER` / `DB_PASSWORD` / `DB_NAME` +- `JWT_SECRET`(必须为强随机值,禁止使用默认值) +- `SERVER_ENV=production` +- `MINI_APP_ID` / `MINI_APP_SECRET`(微信登录) +- `WECHAT_PAY_APIKEY`(支付回调验签,未配置时回调接口直接返回 503) +- `ADMIN_PASSWORD`(管理后台登录,未配置时后台登录禁用) + +## 回滚 + +deploy.sh 健康检查失败会自动回滚;手工回滚使用 `/opt/lunar/backups/` 下的备份包重新执行 deploy.sh。 diff --git a/.gitea/docs/VERSION_MANAGEMENT.md b/.gitea/docs/VERSION_MANAGEMENT.md new file mode 100644 index 0000000..b82a047 --- /dev/null +++ b/.gitea/docs/VERSION_MANAGEMENT.md @@ -0,0 +1,282 @@ +# 版本号管理指南 + +本文档说明如何在小程序和后端服务中管理和显示版本号。 + +## 版本号机制 + +### 小程序版本号 + +- **格式**: `x.y.z`(微信要求每段 0-999) +- **生成规则**: + - `x` = package.json 中的 major 版本 + - `y` = CI 构建编号 / 100 + - `z` = CI 构建编号 % 100 +- **示例**: 构建编号 42 → 版本 `1.0.42`,构建编号 123 → 版本 `1.1.23` + +### 后端版本号 + +- **格式**: `1.0.{BUILD_NUMBER}` +- **生成规则**: 使用 CI 构建编号作为 patch 版本 +- **示例**: 构建编号 42 → 版本 `1.0.42` + +## 版本号注入流程 + +### 小程序(CI 构建时) + +1. CI 读取 `MINIAPP_BUILD_NUMBER`(Gitea Actions 运行编号) +2. 调用 `resolve-version.cjs` 生成版本号 +3. 将版本号写入 `mini/utils/version.js` +4. 上传小程序时使用该版本号 + +**生成的 version.js 示例**: +```javascript +module.exports = { + version: "1.0.42", + buildNumber: "42", + buildTime: "2026-08-08T12:34:56Z", + commitSha: "abc1234", +}; +``` + +### 后端(CI 构建时) + +1. CI 读取 `GITEA_RUN_NUMBER`(Gitea Actions 运行编号) +2. 生成版本号 `1.0.{RUN_NUMBER}` +3. 使用 `ldflags` 注入到 Go 二进制文件 +4. 同时保存到 `bin/version.env` 文件 + +**生成的 version.env 示例**: +``` +APP_VERSION=1.0.42 +APP_BUILD_TIME=2026-08-08T12:34:56Z +APP_COMMIT_SHA=abc1234 +``` + +## 版本号显示 + +### 小程序端 + +#### 1. 个人中心页(settings) + +自动显示版本号和构建信息: + +``` +老黄历小程序 v1.0.42 +构建 #42 (abc1234) +提供农历、黄历、八字、许愿等功能 +``` + +#### 2. 使用版本号组件 + +在其他页面中使用 `version-badge` 组件: + +**wxml**: +```xml + + + + + + + + +``` + +**json**: +```json +{ + "usingComponents": { + "version-badge": "/components/version-badge/version-badge" + } +} +``` + +**显示效果**: +- 简单: `v1.0.42` +- 详细: `v1.0.42 #42 (abc1234)` +- 完整: `v1.0.42 #42 (abc1234) 2026/08/08 12:34` + +### 后端 API + +#### 获取版本信息 + +**接口**: `GET /api/version` + +**响应**: +```json +{ + "version": "1.0.42", + "buildTime": "2026-08-08T12:34:56Z", + "commitSha": "abc1234", + "goVersion": "go1.22" +} +``` + +**使用示例**: +```bash +curl http://localhost:8080/api/version +``` + +## 部署通知中的版本号 + +### 小程序部署通知 + +``` +✅ 小程序开发版上传成功 + +状态: 成功 +仓库: gouki/lunar +分支: main +提交: abc1234 +作者: gouki +工作流: Publish Mini Program Dev Version #42 + +版本: 1.0.42 上传结果已保存到: /opt/lunar/ci-artifacts/miniapp-upload-latest.json +``` + +### 后端部署通知 + +``` +✅ 后端服务部署成功 + +状态: 成功 +仓库: gouki/lunar +分支: main +提交: abc1234 +作者: gouki +工作流: Build and Deploy Server #42 + +版本: v1.0.42 +部署包: /opt/lunar/ci-artifacts/server-deploy-latest.tar.gz +``` + +## 版本号一致性检查 + +### 本地开发环境 + +**小程序**: +```bash +# 查看 package.json 中的版本 +cat mini/package.json | grep version + +# 查看 version.js 中的版本 +cat mini/utils/version.js +``` + +**后端**: +```bash +# 本地运行时版本为 "dev" +curl http://localhost:8080/api/version +``` + +### 生产环境 + +**小程序**: +1. 打开小程序 +2. 进入"我的"页面 +3. 查看"关于"部分的版本号 + +**后端**: +```bash +# 访问生产环境的版本接口 +curl https://your-domain.com/api/version +``` + +### CI 构建产物 + +**小程序**: +```bash +# SSH 到服务器 +ssh doc79 + +# 查看上传结果 +cat /opt/lunar/ci-artifacts/miniapp-upload-latest.json | jq .version +``` + +**后端**: +```bash +# 查看版本信息文件 +cat /opt/lunar/production/current/bin/version.env + +# 或者访问 API +curl http://localhost:8080/api/version +``` + +## 版本号追踪 + +### 通过 Git Commit SHA + +每个版本都包含 Git commit SHA(前 7 位),可以追溯到具体的代码提交: + +```bash +# 查看某个版本的完整 commit 信息 +git show abc1234 + +# 查看某个版本的变更内容 +git log --oneline abc1234^..abc1234 +``` + +### 通过构建编号 + +构建编号对应 Gitea Actions 的运行编号: + +1. 访问 Gitea 网页界面 +2. 进入仓库 → Actions +3. 查找对应编号的运行记录 +4. 查看完整的构建日志和产物 + +## 故障排查 + +### 版本号不正确 + +**问题**: 小程序显示的版本号与预期不符 + +**排查步骤**: +1. 检查 `mini/utils/version.js` 是否被正确生成 +2. 检查 CI 日志中的版本号解析过程 +3. 确认 `MINIAPP_BUILD_NUMBER` 环境变量是否正确传递 + +### 版本号未更新 + +**问题**: 部署后版本号没有变化 + +**可能原因**: +1. CI 构建缓存导致 `version.js` 未被重新生成 +2. 小程序缓存导致旧版本仍在使用 + +**解决方法**: +1. 清除 CI 构建缓存 +2. 在小程序中清除缓存并重新编译 +3. 确认 CI 日志中版本号已更新 + +### 后端版本号显示为 "dev" + +**问题**: 生产环境的 `/api/version` 返回 `"version": "dev"` + +**原因**: 环境变量未正确注入 + +**解决方法**: +1. 检查 CI 构建日志,确认 `ldflags` 注入成功 +2. 检查部署脚本,确认 `version.env` 文件被正确加载 +3. 检查容器环境变量配置 + +## 最佳实践 + +1. **版本号语义化**: 遵循语义化版本规范(SemVer) +2. **版本号可见性**: 在用户界面和 API 中都显示版本号 +3. **版本号追踪**: 通过 commit SHA 和构建编号追踪版本 +4. **版本号一致性**: 确保本地、CI、生产环境的版本号一致 +5. **版本号文档**: 在 CHANGELOG 中记录每个版本的变更 + +## 相关文件 + +- 小程序版本配置: `mini/utils/version.js` +- 小程序版本组件: `mini/components/version-badge/` +- 后端版本接口: `server/internal/handler/version.go` +- 小程序 CI 配置: `.gitea/workflows/miniapp-preview.yml` +- 后端 CI 配置: `.gitea/workflows/server-deploy.yml` +- 版本解析脚本: `mini/ci/resolve-version.cjs` + +## 更新日志 + +- 2026-08-08: 初始版本,支持小程序和后端的版本号注入、显示和追踪 diff --git a/.gitea/scripts/deploy-watch.sh b/.gitea/scripts/deploy-watch.sh new file mode 100644 index 0000000..3aba0c0 --- /dev/null +++ b/.gitea/scripts/deploy-watch.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# 拉取式部署 watcher - 监听 CI 产物目录,发现新版本自动部署 +# 设计原则: +# - CI 只负责构建并把产物写入 /ci-artifacts(runner 与本机同宿主) +# - 部署动作只由服务器本地的本脚本触发,任何 AI/CI 均不通过 SSH 操作服务器 +# 安装方式见 .gitea/docs/PULL_DEPLOY.md(由管理员在服务器上手工执行一次) + +set -u + +ARTIFACT="${ARTIFACT:-/ci-artifacts/server-deploy-latest.tar.gz}" +STATE_FILE="${STATE_FILE:-/opt/lunar/production/.last-deployed-sha256}" +DEPLOY_SCRIPT="${DEPLOY_SCRIPT:-/opt/lunar/scripts/deploy.sh}" +LOG_FILE="${LOG_FILE:-/opt/lunar/deploy-watch.log}" + +log() { + echo "$(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG_FILE" +} + +# 产物不存在时静默等待 +if [ ! -f "$ARTIFACT" ]; then + exit 0 +fi + +# 计算产物指纹 +CURRENT_SHA="$(sha256sum "$ARTIFACT" | awk '{print $1}')" +LAST_SHA="" +if [ -f "$STATE_FILE" ]; then + LAST_SHA="$(cat "$STATE_FILE")" +fi + +# 无变化则跳过 +if [ "$CURRENT_SHA" = "$LAST_SHA" ]; then + exit 0 +fi + +log "发现新版本产物 sha256=${CURRENT_SHA:0:12},开始部署" + +# 执行部署;成功后才记录指纹(失败时下次轮询会重试) +if bash "$DEPLOY_SCRIPT" "$ARTIFACT" >> "$LOG_FILE" 2>&1; then + mkdir -p "$(dirname "$STATE_FILE")" + printf '%s' "$CURRENT_SHA" > "$STATE_FILE" + log "部署成功" +else + log "部署失败,保留旧指纹,等待下次轮询重试或人工介入" + exit 1 +fi diff --git a/.gitea/scripts/deploy.sh b/.gitea/scripts/deploy.sh index fb9fdc6..068dade 100644 --- a/.gitea/scripts/deploy.sh +++ b/.gitea/scripts/deploy.sh @@ -1,5 +1,6 @@ #!/bin/bash # 自动部署脚本 - 部署到 1Panel Docker 容器 +# 设计原则:拉取式部署,只在服务器本地执行,CI 不通过 SSH 触发 # 使用方法: ./deploy.sh # deploy_package_path: 部署包路径(如 /ci-artifacts/server-deploy-latest.tar.gz) @@ -9,6 +10,7 @@ DEPLOY_PACKAGE="$1" CONTAINER_NAME="${CONTAINER_NAME:-lunar-server}" DEPLOY_DIR="${DEPLOY_DIR:-/opt/lunar/production}" BACKUP_DIR="${BACKUP_DIR:-/opt/lunar/backups}" +HEALTH_URL="${HEALTH_URL:-http://localhost:8080/api/version}" # 颜色输出 RED='\033[0;31m' @@ -28,6 +30,18 @@ log_error() { echo -e "${RED}[ERROR]${NC} $1" } +# 回滚函数:恢复 current.old 并重启容器 +rollback() { + if [ -d "$DEPLOY_DIR/current.old" ]; then + rm -rf "$DEPLOY_DIR/current" + mv "$DEPLOY_DIR/current.old" "$DEPLOY_DIR/current" + docker restart "$CONTAINER_NAME" || log_error "回滚后容器重启失败,需人工介入" + log_warn "已回滚到上一版本" + else + log_error "无可回滚版本,需人工介入" + fi +} + # 检查部署包是否存在 if [ ! -f "$DEPLOY_PACKAGE" ]; then log_error "部署包不存在: $DEPLOY_PACKAGE" @@ -52,7 +66,7 @@ if [ -d "$DEPLOY_DIR/current" ]; then tar -czf "$BACKUP_FILE" -C "$DEPLOY_DIR" current/ || log_warn "备份失败,继续部署" fi -# 解压新版本 +# 解压新版本到暂存目录(不碰 current,避免部署空窗) log_info "解压部署包..." TEMP_DIR=$(mktemp -d) tar -xzf "$DEPLOY_PACKAGE" -C "$TEMP_DIR" @@ -64,15 +78,23 @@ if [ ! -d "$TEMP_DIR/deploy" ]; then exit 1 fi -# 部署新版本 -log_info "部署新版本..." -rm -rf "$DEPLOY_DIR/current" -mv "$TEMP_DIR/deploy" "$DEPLOY_DIR/current" -rm -rf "$TEMP_DIR" +# 校验二进制存在 +if [ ! -f "$TEMP_DIR/deploy/bin/server" ]; then + log_error "部署包缺少 bin/server 二进制" + rm -rf "$TEMP_DIR" + exit 1 +fi +chmod +x "$TEMP_DIR/deploy/bin/server" -# 设置权限 -log_info "设置文件权限..." -chmod +x "$DEPLOY_DIR/current/bin/server" || log_warn "设置可执行权限失败" +# 部署新版本(目录级交换,接近原子) +log_info "部署新版本..." +mv "$TEMP_DIR/deploy" "$DEPLOY_DIR/current.new" +rm -rf "$DEPLOY_DIR/current.old" +if [ -d "$DEPLOY_DIR/current" ]; then + mv "$DEPLOY_DIR/current" "$DEPLOY_DIR/current.old" +fi +mv "$DEPLOY_DIR/current.new" "$DEPLOY_DIR/current" +rm -rf "$TEMP_DIR" # 检查 Docker 容器是否存在 if ! docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then @@ -95,25 +117,30 @@ log_info "重启 Docker 容器: $CONTAINER_NAME" if docker restart "$CONTAINER_NAME"; then log_info "✅ 容器重启成功" else - log_error "❌ 容器重启失败" + log_error "❌ 容器重启失败,回滚到上一版本" + rollback exit 1 fi -# 等待容器启动 -log_info "等待容器启动..." -sleep 3 +# 健康检查:轮询 HTTP 接口确认服务真正可用 +log_info "执行健康检查: $HEALTH_URL" +HEALTHY=0 +for i in $(seq 1 10); do + sleep 2 + if curl -fsS --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then + HEALTHY=1 + break + fi +done -# 检查容器状态 -if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then - log_info "✅ 容器运行正常" - - # 显示容器日志(最后 20 行) - log_info "容器日志(最后 20 行):" - docker logs --tail 20 "$CONTAINER_NAME" +if [ "$HEALTHY" = "1" ]; then + log_info "✅ 健康检查通过" + curl -fsS --max-time 5 "$HEALTH_URL" && echo "" + rm -rf "$DEPLOY_DIR/current.old" else - log_error "❌ 容器未运行" - log_error "容器日志:" - docker logs --tail 50 "$CONTAINER_NAME" + log_error "❌ 健康检查失败,回滚到上一版本" + docker logs --tail 50 "$CONTAINER_NAME" || true + rollback exit 1 fi diff --git a/.gitea/workflows/miniapp-preview.yml b/.gitea/workflows/miniapp-preview.yml index fb80c62..c0a9493 100644 --- a/.gitea/workflows/miniapp-preview.yml +++ b/.gitea/workflows/miniapp-preview.yml @@ -25,7 +25,7 @@ jobs: run: npm ci - name: Run tests working-directory: mini - run: npm test -- --runInBand + run: npm test - name: Configure production API and app version working-directory: mini env: diff --git a/.gitea/workflows/server-deploy.yml b/.gitea/workflows/server-deploy.yml index 4535a6d..d35fe5d 100644 --- a/.gitea/workflows/server-deploy.yml +++ b/.gitea/workflows/server-deploy.yml @@ -62,20 +62,6 @@ jobs: echo "Version info saved to bin/version.env:" cat bin/version.env - - name: Build frontend (if exists) - working-directory: server - run: | - if [ -d "web" ]; then - echo "Building frontend..." - cd web - if [ -f "package.json" ]; then - npm ci - npm run build - fi - else - echo "No frontend directory found, skipping frontend build" - fi - - name: Create deployment package working-directory: server run: | @@ -83,8 +69,9 @@ jobs: cp -r bin/ deploy/ cp -r migrations/ deploy/ cp -r scripts/ deploy/ - if [ -d "web/dist" ]; then - cp -r web/dist/ deploy/public/ + # 管理后台静态资源必须随包发布(服务以相对路径 ./web 提供) + if [ -d "web" ]; then + cp -r web/ deploy/web/ fi tar -czf deploy.tar.gz deploy/ @@ -107,8 +94,8 @@ jobs: run: | VERSION="1.0.${GITEA_RUN_NUMBER}" bash .gitea/scripts/notify.sh success \ - "后端服务部署成功" \ - "版本: v${VERSION}\n部署包: /opt/lunar/ci-artifacts/server-deploy-latest.tar.gz" + "后端构建成功" \ + "版本: v${VERSION}\n产物: /opt/lunar/ci-artifacts/server-deploy-latest.tar.gz\n部署由服务器侧 watcher 自动拉取完成" - name: Send failure notification if: failure() @@ -118,7 +105,7 @@ jobs: DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} run: | bash .gitea/scripts/notify.sh failure \ - "后端服务部署失败" \ + "后端构建失败" \ "请检查 Actions 日志了解失败原因" - name: Cleanup