From a50d48e5d48a325599e37d58d5e2f2aedddb682c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=80=BB=E5=B7=A5?= Date: Sat, 29 Aug 2026 13:43:04 +0800 Subject: [PATCH] =?UTF-8?q?=E9=83=A8=E7=BD=B2(HEL-235):=20=E5=9B=BA?= =?UTF-8?q?=E5=8C=96=20git=20=E5=BD=92=E6=A1=A3=E6=B5=81=E5=BC=8F=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E4=B8=BA=E5=94=AF=E4=B8=80=E5=AE=89=E5=85=A8=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: multica-agent --- DOCKER_DEPLOY.md | 35 +++++++++++++++-- tools/README.md | 6 +++ tools/build_image.sh | 94 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 3 deletions(-) create mode 100755 tools/build_image.sh diff --git a/DOCKER_DEPLOY.md b/DOCKER_DEPLOY.md index 378050d..a1526d5 100644 --- a/DOCKER_DEPLOY.md +++ b/DOCKER_DEPLOY.md @@ -149,7 +149,31 @@ docker compose restart xiaobai-review docker compose down ``` -### 使用 Gitea 更新程序(推荐) +### 镜像构建的唯一安全入口(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,解析失败立即中止,绝不使用本地脏状态或服务器旧目录; +- 镜像 tag 必须以 `-<提交短号7位>` 结尾(如 `hel234-cefc869`),禁止 `latest`、`rollback-*`; +- 通过 `git archive <提交> | ssh 部署机 docker build -` 流式构建,服务器上不存在构建用工作树; +- 构建后回读镜像 label 里的 `org.opencontainers.image.revision`,与预期提交不一致则删除镜像并中止; +- 每次构建在部署机 `~/xiaobai-build/BUILD_LOG.tsv` 留痕,可追溯每个镜像的来源提交。 + +构建只产出镜像,不启动、不替换任何容器;换版用新 tag 起新容器,回滚用既有镜像 tag 重跑。 + +### 使用 Gitea 更新程序(旧方式,生产机禁用) 代码仓库为: @@ -174,7 +198,9 @@ cd /opt/xiaobai-review `data/private-mentor-skills/` 复制到服务器项目的同名 `data` 目录,并保持目录仅由 部署账号和容器运行用户读取。该内容不会通过 Gitea 同步。 -每次更新前先创建 SQLite 一致性备份,再拉取并重建容器: +每次更新前先创建 SQLite 一致性备份,再拉取并重建容器(注意:`docker compose up -d --build` +从服务器本地工作树构建,仅适用于来源可信的全新环境;生产机 `192.168.200.11` 禁用, +请用 `tools/build_image.sh` 构建后换容器): ```bash cd /opt/xiaobai-review @@ -189,7 +215,10 @@ curl --fail http://127.0.0.1:8765/api/health 数据库迁移会在新容器启动时自动执行。若 `git pull --ff-only` 提示本地代码有修改, 先用 `git status` 查明原因,不要用强制重置覆盖 `.env` 或 `data`。 -### 不使用 Git 时更新 +### 不使用 Git 时更新(生产机禁用) + +`docker compose build` 会从服务器本地目录构建,来源提交不可追溯。生产机 +`192.168.200.11` 上禁止使用本节方式,一律改用上一节的 `tools/build_image.sh`。 重新上传代码后执行: diff --git a/tools/README.md b/tools/README.md index 417e3e4..639105c 100644 --- a/tools/README.md +++ b/tools/README.md @@ -17,6 +17,12 @@ registry, and verification tools. `backend/features/*/routes.py` owners. - `python tools/build_architecture_inventory.py [--check]`: generate or verify `config/architecture-inventory.json` from the current source tree. +- `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 + 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`. `verify_baseline.py` does not inspect a parent checkout or skip tests according to files outside this application. Historical comparison scripts were retired after final standalone acceptance; diff --git a/tools/build_image.sh b/tools/build_image.sh new file mode 100755 index 0000000..08213d4 --- /dev/null +++ b/tools/build_image.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# 小白复盘唯一安全构建入口(HEL-235 固化) +# 方式:从明确 Git 提交 git archive 流式传输到部署机 docker build,不使用任何服务器工作树。 +# 铁律:禁止在服务器目录(如 /opt/1panel/docker/compose/xiaobaifupan)里 docker build; +# 禁止构建 latest 等不带提交短号的 tag;严禁向 192.168.200.36 构建或部署。 +set -euo pipefail + +HOST_DEFAULT="moxiaobai@192.168.200.11" +REPO_NAME="xiaobai-review" + +usage() { + cat <<'EOF' +用法: tools/build_image.sh + 提交号(完整或前缀),必须能被 origin 解析;构建前会自动 fetch + 镜像 tag,必须以 -<提交短号7位> 结尾,锁定镜像来源;禁止 latest、rollback-* +示例: tools/build_image.sh cefc86917d89 verify-hel235-cefc869 +说明: 仅构建镜像,不启动、不替换任何容器;换版与回滚另行人工执行。 +EOF + exit 2 +} + +[ $# -eq 2 ] || usage +COMMIT="$1" +TAG="$2" +HOST="${XB_BUILD_HOST:-$HOST_DEFAULT}" + +case "$HOST" in + *192.168.200.36*) + echo "拒绝:192.168.200.36 已永久废弃,严禁在其上构建或部署。" >&2 + exit 1 + ;; +esac + +cd "$(git rev-parse --show-toplevel)" + +echo "==> 同步远端引用" +git fetch origin --prune --quiet + +FULL_SHA="$(git rev-parse --verify --quiet "${COMMIT}^{commit}" || true)" +if [ -z "$FULL_SHA" ]; then + echo "拒绝:提交 ${COMMIT} 无法解析。构建源必须锁定到已推送 origin 的明确提交。" >&2 + exit 1 +fi +SHORT="${FULL_SHA:0:7}" +SUBJECT="$(git log -1 --format=%s "$FULL_SHA")" + +case "$TAG" in + latest) + echo "拒绝:禁止构建 latest,模糊 tag 无法追溯来源提交。" >&2 + exit 1 + ;; + rollback-*) + echo "拒绝:rollback-* 是部署时对既有镜像的人工 docker tag,不允许用来构建。" >&2 + exit 1 + ;; +esac +if [[ "$TAG" != *-"$SHORT" ]]; then + echo "拒绝:tag「${TAG}」必须以 -${SHORT} 结尾,保证镜像 tag 与来源提交一一对应。" >&2 + exit 1 +fi + +echo "==> 构建计划" +echo " 提交: ${FULL_SHA} ${SUBJECT}" +echo " 镜像: ${REPO_NAME}:${TAG} @ ${HOST}" +echo " 方式: git archive 流式构建(不读取服务器上任何代码目录)" + +echo "==> 流式构建开始" +git archive --format=tar "$FULL_SHA" \ + | ssh -o BatchMode=yes "$HOST" docker build --rm \ + -t "${REPO_NAME}:${TAG}" \ + --label "org.opencontainers.image.revision=${FULL_SHA}" \ + --label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --label "org.opencontainers.image.source=git-archive-stream" \ + - + +echo "==> 回读校验镜像内记录的提交号" +GOT="$(ssh -o BatchMode=yes "$HOST" docker image inspect "${REPO_NAME}:${TAG}" \ + --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" +if [ "$GOT" != "$FULL_SHA" ]; then + echo "校验失败:镜像 revision=${GOT},期望 ${FULL_SHA}。删除不可信镜像,中止。" >&2 + ssh -o BatchMode=yes "$HOST" docker rmi "${REPO_NAME}:${TAG}" >/dev/null 2>&1 || true + exit 1 +fi + +IMAGE_ID="$(ssh -o BatchMode=yes "$HOST" docker image inspect "${REPO_NAME}:${TAG}" --format '{{.Id}}')" +SHORT_ID="${IMAGE_ID##*:}" + +ssh -o BatchMode=yes "$HOST" \ + 'mkdir -p ~/xiaobai-build && printf "%s\t%s\t%s\t%s\tgit-archive-stream\n" "$(date "+%F %T")" "$1" "$2" "$3" >> ~/xiaobai-build/BUILD_LOG.tsv' \ + "build-image" "${REPO_NAME}:${TAG}" "${FULL_SHA}" "${SHORT_ID}" + +echo "==> 完成" +echo " ${REPO_NAME}:${TAG} (${SHORT_ID})" +echo " 来源提交 ${FULL_SHA} 已写入镜像 label 与 ~/xiaobai-build/BUILD_LOG.tsv"