diff --git a/DOCKER_DEPLOY.md b/DOCKER_DEPLOY.md index 4626e5a..bc5df13 100644 --- a/DOCKER_DEPLOY.md +++ b/DOCKER_DEPLOY.md @@ -165,88 +165,58 @@ docker compose restart xiaobai-review docker compose down ``` -### 镜像构建的唯一安全入口(2026-08 HEL-235 起) +### 服务器本地目录更新与构建(日常推荐) -生产机 `192.168.200.11` 上的 `/opt/1panel/docker/compose/xiaobaifupan` 只是历史文件树: -不是 Git 仓库、内容停在旧提交、与线上镜像不一致,且其 `compose.yaml` 会把构建结果打进 -`xiaobai-review:latest`。**禁止在该目录(或任何服务器工作树)里 `docker build` / -`docker compose build`**,否则会把已上线功能悄悄打回旧版。 - -唯一安全构建方式是在有仓库检出、能免密 SSH 到部署机的机器上运行: - -```bash -tools/build_image.sh <提交号> <镜像tag> -# 示例:tools/build_image.sh cefc86917d89 verify-hel235-cefc869 -``` - -该脚本的行为约束: - -- 先 `git fetch`,再把提交号解析为完整 SHA,解析失败立即中止,绝不使用本地脏状态或服务器旧目录; -- 构建前读取当前线上容器镜像的 `org.opencontainers.image.revision`,用 Git 祖先关系确认候选提交包含线上全部历史;落后 `main`、旁支或错误提交会直接退出,并打印线上提交、候选提交、文件差异和将丢失的提交; -- 镜像 tag 必须以 `-<提交短号7位>` 结尾(如 `hel234-cefc869`),禁止 `latest`、`rollback-*`; -- 通过 `git archive <提交> | ssh 部署机 docker build -` 流式构建,服务器上不存在构建用工作树; -- 构建后回读镜像 label 里的 `org.opencontainers.image.revision`,与预期提交不一致则删除镜像并中止; -- 每次构建在部署机 `~/xiaobai-build/BUILD_LOG.tsv` 留痕,可追溯每个镜像的来源提交。 - -构建只产出镜像,不启动、不替换任何容器;换版用新 tag 起新容器,回滚用既有镜像 tag 重跑。 - -### 使用 Gitea 更新程序(旧方式,生产机禁用) - -代码仓库为: +生产机 `192.168.200.11` 的 `/opt/1panel/docker/compose/xiaobaifupan` 自 2026-08-29(HEL-235B) +起已是受 Git 管理的工作目录,只跟踪 Gitea `main`(仓库 +`http://192.168.200.36:3200/leefer/xiaobai-review.git`)。由于目录顶层归 root, +`.git` 存放在部署账号家目录(外部 Git 目录方案): ```text -http://192.168.200.36:3200/leefer/xiaobaifupan.git +~/xiaobai-build/repos/xiaobai-review.git Git 元数据(分支/历史/索引) +/opt/1panel/docker/compose/xiaobaifupan 工作目录(程序文件本体) +~/xiaobai-build/update-from-main.sh 一键更新+构建入口 +~/xiaobai-git 便捷查看(status/log/diff) ``` -首次在服务器部署代码时,可以直接克隆到目标目录: +日常更新只需要在服务器上执行一条命令: ```bash -sudo mkdir -p /opt/xiaobai-review -sudo chown "$USER":"$USER" /opt/xiaobai-review -git clone http://192.168.200.36:3200/leefer/xiaobaifupan.git /opt/xiaobai-review -cd /opt/xiaobai-review +~/xiaobai-build/update-from-main.sh # 更新到 main 并构建 main-<短号> 镜像 +~/xiaobai-build/update-from-main.sh verify-tag main-a8732f5 # 部署前复核镜像与 main 一致 ``` -私有仓库会提示输入 Gitea 用户名和密码或访问令牌。不要把密码写入仓库 URL、 -`compose.yaml` 或脚本。然后把原 `.env` 与 `data/` 放回该目录;这两项已被 Git -忽略,后续拉取代码不会覆盖数据库与密钥。 +脚本在构建前强制完成五道校验,任一不符立即停止、不产出镜像: -如需部署管理员私有问师,通过 NAS 文件管理器将本地 -`data/private-mentor-skills/` 复制到服务器项目的同名 `data` 目录,并保持目录仅由 -部署账号和容器运行用户读取。该内容不会通过 Gitea 同步。 +1. `git fetch` 成功(连不上 Gitea 即停); +2. 必须在 `main` 分支(智能体不得用功能分支直接当正式线); +3. 工作区无未提交改动、无多余文件; +4. 只允许快进合并到 `origin/main`(分叉即停);main 新增/删除顶层文件时会给出 + 需管理员执行的精确清单(目录顶层归 root); +5. 构建后回读镜像 `org.opencontainers.image.revision`,与 `main` 提交不一致则删除镜像。 -每次更新前先创建 SQLite 一致性备份,再拉取并重建容器(注意:`docker compose up -d --build` -从服务器本地工作树构建,仅适用于来源可信的全新环境;生产机 `192.168.200.11` 禁用, -请用 `tools/build_image.sh` 构建后换容器): +镜像 tag 固定为 `main-<提交短号7位>`(不带提交号的模糊 tag 一律禁止);每次构建在 +`~/xiaobai-build/BUILD_LOG.tsv` 留痕。构建只产出镜像,不启动、不替换容器;换版与 +回滚步骤见 `~/xiaobai-build/README.md`。 -```bash -cd /opt/xiaobai-review -docker compose exec -T xiaobai-review python -c "import sqlite3; s=sqlite3.connect('/app/data/review.db'); d=sqlite3.connect('/app/data/review-before-update.db'); s.backup(d); d.close(); s.close()" -git pull --ff-only origin main -docker compose up -d --build -docker compose ps -curl --fail http://127.0.0.1:8765/api/health -``` +`compose.yaml` 的镜像名与 revision 标签同样做了强校验:直接 `docker compose up -d --build` +会因缺少 `XIAOBAI_GIT_REV` / `XIAOBAI_GIT_SHORT` 变量而拒绝执行,避免再出现构建进 +`latest` 的模糊版本。需要用 compose 时先 `export` 这两个变量(值以 +`~/xiaobai-git rev-parse HEAD` 为准),或直接用上面的脚本。 -`docker compose up -d --build` 会原地替换应用容器,不删除宿主机的 `data` 目录。 -数据库迁移会在新容器启动时自动执行。若 `git pull --ff-only` 提示本地代码有修改, -先用 `git status` 查明原因,不要用强制重置覆盖 `.env` 或 `data`。 +### 智能体高级入口:Git 归档流式构建 -### 不使用 Git 时更新(生产机禁用) +有仓库检出、能免密 SSH 到部署机的智能体可以用 `tools/build_image.sh <提交号> <镜像tag>` +从任意明确提交流式构建(`git archive | ssh docker build`),tag 同样必须以 +`-<提交短号7位>` 结尾,构建后回读 revision 校验并留痕。用于在服务器不便拉取时的 +应急构建;日常正式线仍应走 `main`。 -`docker compose build` 会从服务器本地目录构建,来源提交不可追溯。生产机 -`192.168.200.11` 上禁止使用本节方式,一律改用上一节的 `tools/build_image.sh`。 +### 历史方式(已废弃) -重新上传代码后执行: - -```bash -docker compose down -docker compose build --pull -docker compose up -d -``` - -`docker compose down` 不会删除宿主机的 `data` 目录。不要使用带有手工删除 -`data` 目录的清理命令。 +早期文档建议在服务器重新 `git clone` 一份或手工上传代码后 `docker compose up --build`。 +这两条路径已废弃:服务器上**只允许存在一个受管工作目录**(上述 +`/opt/1panel/docker/compose/xiaobaifupan`),任何脱离 Git 校验的本地构建都会把 +来源提交变成不可追溯状态,禁止使用。 ## 7. 备份与恢复 diff --git a/compose.yaml b/compose.yaml index c252d8a..a97a8d2 100644 --- a/compose.yaml +++ b/compose.yaml @@ -3,7 +3,9 @@ services: build: context: . dockerfile: Dockerfile - image: xiaobai-review:latest + labels: + org.opencontainers.image.revision: "${XIAOBAI_GIT_REV:?必须先设置 XIAOBAI_GIT_REV=当前 main 完整提交号(或改用 tools/update_from_main.sh)}" + image: xiaobai-review:main-${XIAOBAI_GIT_SHORT:?必须先设置 XIAOBAI_GIT_SHORT=7位提交短号(或改用 tools/update_from_main.sh)} container_name: xiaobai-review ports: - "0.0.0.0:8765:8765/tcp" diff --git a/tools/README.md b/tools/README.md index cc5c632..5dcc15c 100644 --- a/tools/README.md +++ b/tools/README.md @@ -20,12 +20,18 @@ registry, and verification tools. - `python tools/backfill_recent_snapshots.py --account [--lookback 60] [--dry-run]`: auditable recent trading-day dashboard snapshot backfill. See `docs/maintenance/行情历史补档.md`. -- `bash tools/build_image.sh `: the only sanctioned way to build the - production Docker image. Streams `git archive ` to the deploy host over SSH - (default `moxiaobai@192.168.200.11`), refuses tags that do not end with the commit +- `tools/update_from_main.sh` (deployed to the server as + `~/xiaobai-build/update-from-main.sh`): the server-side update-and-build entry for + the managed local worktree at `/opt/1panel/docker/compose/xiaobaifupan`. Fetches + Gitea `main`, enforces branch/clean/fast-forward checks, builds a + `main-` tagged image with the revision label, and verifies the label + after the build. `tools/xiaobai-git` is the matching git wrapper for that + worktree (`status`/`log`/`diff`). +- `bash tools/build_image.sh `: agent-grade entry that streams + `git archive ` to the deploy host over SSH (default + `moxiaobai@192.168.200.11`), refuses tags that do not end with the commit short SHA, verifies the revision label after the build, and appends a record to - `~/xiaobai-build/BUILD_LOG.tsv` on the host. Building from any server-side working - tree is forbidden; see `DOCKER_DEPLOY.md`. Before building, it runs + `~/xiaobai-build/BUILD_LOG.tsv` on the host. Before building, it runs `tools/check_deploy_baseline.sh` so the candidate commit must contain the currently running container's Git revision as an ancestor. - `bash tools/check_deploy_baseline.sh [--live-revision ]`: deployment diff --git a/tools/update_from_main.sh b/tools/update_from_main.sh new file mode 100755 index 0000000..e2d624c --- /dev/null +++ b/tools/update_from_main.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# 小白复盘服务器本地目录安全更新/构建入口(HEL-235B 固化) +# 作用:把 /opt/1panel/docker/compose/xiaobaifupan 的 Git 工作目录安全快进到 Gitea main, +# 校验“本地 HEAD = origin/main = 镜像 revision”后,从本地目录构建带提交号的镜像。 +# 禁止:不从 main 构建;不使用不带提交短号的 tag;本地有改动/落后/分叉时一律停止。 +# 说明:目录顶层归 root,本脚本用“截断写入”绕开 git 对顶层文件 unlink+重建的权限要求; +# 但 main 新增/删除顶层文件时无法自动处理,会列出需管理员执行的精确清单。 +set -euo pipefail + +GIT_DIR_PATH="$HOME/xiaobai-build/repos/xiaobai-review.git" +WORK_TREE="/opt/1panel/docker/compose/xiaobaifupan" +IMAGE_REPO="xiaobai-review" +LOG_FILE="$HOME/xiaobai-build/BUILD_LOG.tsv" +MODE="${1:-build}" + +g() { git --git-dir="$GIT_DIR_PATH" --work-tree="$WORK_TREE" "$@"; } + +refuse() { printf '拒绝:%s\n' "$*" >&2; exit 1; } + +[ "$MODE" = "build" ] || [ "$MODE" = "verify-tag" ] || refuse "未知子命令「${MODE}」(可用:build / verify-tag )" +[ -d "$GIT_DIR_PATH" ] || refuse "Git 目录不存在:$GIT_DIR_PATH" + +echo "==> 拉取 Gitea origin/main" +g fetch --quiet origin main || refuse "无法连接 Gitea 拉取 origin/main" + +echo "==> 检查分支与工作区" +BRANCH="$(g symbolic-ref --short HEAD 2>/dev/null || true)" +[ "$BRANCH" = "main" ] || refuse "当前不在 main 分支(${BRANCH:-detached}),停止" +DIRTY="$(g status --porcelain)" +[ -z "$DIRTY" ] || refuse "本地目录有未提交改动或多余文件,先处理再构建: +$DIRTY" + +LOCAL_HEAD="$(g rev-parse HEAD)" +REMOTE_HEAD="$(g rev-parse origin/main)" +if [ "$LOCAL_HEAD" != "$REMOTE_HEAD" ]; then + TOP_AD="$(g diff --name-status HEAD origin/main | grep -E "^[AD][[:space:]]+[^/]+$" || true)" + [ -z "$TOP_AD" ] || refuse "main 相比本地新增/删除了顶层文件,目录顶层归 root,需管理员执行: +$TOP_AD" + TOP_MOD="$(g diff --name-status HEAD origin/main | grep -E "^M[[:space:]]+[^/]+$" | awk '{print $2}' || true)" + if [ -n "$TOP_MOD" ]; then + echo "==> 预写入顶层改动文件(顶层目录无删除权限,改为截断写入)" + while IFS= read -r f; do g show "origin/main:$f" > "$WORK_TREE/$f"; done <<< "$TOP_MOD" + fi + echo "==> 快进合并到 origin/main" + g merge --ff-only origin/main >/dev/null 2>&1 || refuse "无法快进合并 origin/main(历史分叉),停止" + DIRTY="$(g status --porcelain)" + [ -z "$DIRTY" ] || refuse "快进后工作区仍不一致,停止: +$DIRTY" + LOCAL_HEAD="$(g rev-parse HEAD)" +fi +[ "$LOCAL_HEAD" = "$REMOTE_HEAD" ] || refuse "本地 HEAD 与 origin/main 不一致,停止" +SHORT="${LOCAL_HEAD:0:7}" +echo "==> 校验通过:本地 HEAD = origin/main = ${LOCAL_HEAD}(${SHORT})" + +if [ "$MODE" = "verify-tag" ]; then + TAG="${2:?用法: update-from-main.sh verify-tag }" + LABEL="$(docker image inspect "${IMAGE_REPO}:${TAG}" \ + --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' 2>/dev/null)" \ + || refuse "镜像 ${IMAGE_REPO}:${TAG} 不存在" + [ "$LABEL" = "$LOCAL_HEAD" ] || refuse "镜像 revision(${LABEL})与当前 main(${LOCAL_HEAD})不一致,禁止部署" + echo "==> 通过:${IMAGE_REPO}:${TAG} 的 revision 与 main 一致,可以部署" + exit 0 +fi + +TAG="main-${SHORT}" +echo "==> 从本地目录构建 ${IMAGE_REPO}:${TAG}" +docker build --rm -t "${IMAGE_REPO}:${TAG}" \ + --label "org.opencontainers.image.revision=${LOCAL_HEAD}" \ + --label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + "$WORK_TREE" 2>&1 | tail -5 + +echo "==> 回读校验镜像 revision" +GOT="$(docker image inspect "${IMAGE_REPO}:${TAG}" \ + --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" +if [ "$GOT" != "$LOCAL_HEAD" ]; then + docker rmi "${IMAGE_REPO}:${TAG}" >/dev/null 2>&1 || true + refuse "镜像 revision(${GOT})与 main(${LOCAL_HEAD})不一致,已删除镜像" +fi +IMAGE_ID="$(docker image inspect "${IMAGE_REPO}:${TAG}" --format '{{.Id}}' | cut -c8-19)" +mkdir -p "$(dirname "$LOG_FILE")" +printf '%s\t%s\t%s\t%s\tlocal-worktree\n' \ + "$(date '+%F %T')" "${IMAGE_REPO}:${TAG}" "${LOCAL_HEAD}" "${IMAGE_ID}" >> "$LOG_FILE" + +cat < 完成:${IMAGE_REPO}:${TAG}(revision=${LOCAL_HEAD}) + 部署需人工确认,参考 ~/xiaobai-build/README.md 的换版与回滚步骤。 +EOF diff --git a/tools/xiaobai-git b/tools/xiaobai-git new file mode 100755 index 0000000..9793b0b --- /dev/null +++ b/tools/xiaobai-git @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# 查看服务器本地目录 Git 状态的便捷入口:xiaobai-git status / log / diff 等 +exec git \ + --git-dir="$HOME/xiaobai-build/repos/xiaobai-review.git" \ + --work-tree="/opt/1panel/docker/compose/xiaobaifupan" \ + "$@"