Files
姚章浩 013ce4a24e
build / build (push) Successful in 1m3s
fix(version): 补全开发快照记录与同步交付闭环
收录 main、注释开发快照和完整运行 SHA;开发说明绑定原提交,补记 0.1.1–0.1.25。
进行中同步按仓库合并,完成后重新执行;启动入队,部署脚本读回版本证据。
开发快照独立于正式标签与 Release,既有冲突标签不移动。

已运行:整仓 make verify,Go 673 通过、前端 274 通过,92 个已声明可选跳过,必需跳过 0。
隔离 PostgreSQL 重复同步队列用例及 -race 通过;未更新现有 8088、未修改生产问答。
2026-10-18 18:11:49 +08:00

135 lines
8.2 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# 重建并上线 governance api + web(本机 docker build 需要宿主代理 + --network host)
set -u
cd "$(dirname "${BASH_SOURCE[0]}")/.." || exit 1
[ -z "$(git status --porcelain --untracked-files=normal)" ] || { printf "%s\n" "REFUSED_DIRTY_SOURCE"; exit 1; }
TS=$(date +%Y%m%d-%H%M%S)
PROXY_ARGS=(--build-arg HTTP_PROXY=http://127.0.0.1:7890 --build-arg HTTPS_PROXY=http://127.0.0.1:7890 --build-arg NO_PROXY=localhost,127.0.0.1 --build-arg GOPROXY=https://goproxy.cn,direct)
echo "=== 1. 打回滚标签 ($TS)"
docker tag knowledge-governance-p1-api:latest "knowledge-governance-p1-api:rollback-$TS" || exit 1
docker tag knowledge-governance-p1-web:latest "knowledge-governance-p1-web:rollback-$TS" || exit 1
echo "=== 1a. 上线前校验 .env 的键值形状(一行一值)"
# 2026-10-14 实测事故:`.env` 最后一行没有结尾换行、又被追加新键 ⇒ 两行粘成一行
# ⇒ VERSION_HISTORY_GITEA_BASE_URL 变成 "http://host:3000VERSION_HISTORY_LINK_ORIGIN=…"
# ⇒ 配置校验按设计拒绝启动 ⇒ api/worker 崩溃重启(平台整体不可用)。
# 这里在**上线前**用形状判定拦住:值里再出现 `KEY=` 或键名不合法,直接拒绝部署。
if [ -f .env ]; then
python3 - <<'ENVCHECK' || exit 1
import pathlib
import re
import sys
problems = []
for number, raw in enumerate(pathlib.Path(".env").read_text(encoding="utf-8").splitlines(), 1):
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", key.strip()):
problems.append((number, line, "键名不合法"))
elif re.search(r"[A-Z][A-Z0-9_]{3,}=", value):
# 不能用 \b:粘行处是 "…:3000VERSION_HISTORY_…",数字紧邻字母,没有词边界。
problems.append((number, line, "值里又出现 KEY=(两行粘在一起)"))
if problems:
print("ENV_VALUE_LOOKS_MERGED(.env 有坏值,拒绝上线)")
for number, line, why in problems:
print(f" line {number}: {why}: {line[:120]}")
sys.exit(1)
print(" .env 键值形状 OK")
ENVCHECK
fi
echo "=== 1b. 构建身份(构建期注入:页面「当前运行」只认产物里的完整 SHA 精确匹配)"
# 唯一来源是 scripts/version.py --identity(与 Makefile、CI 同一份规则):不注入 ⇒ 页面只能显示"未知"。
IDENTITY_ARGS=()
while IFS= read -r line; do IDENTITY_ARGS+=("$line"); done < <(python3 scripts/version.py --identity 2>/dev/null | python3 -c '
import json, sys
try:
d = json.load(sys.stdin)
except Exception:
sys.exit(0)
release = d.get("release_version") or ""
count = d.get("commits_since_release")
print("--build-arg=VERSION=" + (release or "dev"))
print("--build-arg=RELEASE_VERSION=" + release)
print("--build-arg=BUILD_KIND=" + (d.get("build_kind") or "development"))
print("--build-arg=COMMITS_SINCE_RELEASE=" + ("" if count is None else str(count)))
print("--build-arg=COMMIT_SHA=" + (d.get("commit_sha") or "unknown"))
print("--build-arg=BUILD_TIME=" + (d.get("built_at") or "unknown"))
')
if [ "${#IDENTITY_ARGS[@]}" -lt 6 ]; then
echo "IDENTITY_UNAVAILABLE"
echo " 取不到构建身份(scripts/version.py --identity 失败)⇒ 拒绝继续:"
echo " 否则构建出来的产物无法被「版本记录」页识别成「当前运行」,只会显示未知。"
exit 1
fi
printf ' %s\n' "${IDENTITY_ARGS[@]}"
echo "=== 1c. 保留上一版构建的静态资源(CDN/浏览器可能还持有旧 index.html)"
# 只保留**当前正在跑的那一版**:旧 index.html 的旧哈希引用仍能取到文件,
# 页面不会因为某个 JS/CSS 404 而空白或"裸奔"。目录只读挂载进 web 容器。
LEGACY_DIR="deploy/legacy-assets"
PREV_WEB_IMAGE=$(docker inspect -f '{{.Image}}' knowledge-governance-p1-web-1 2>/dev/null || true)
mkdir -p "$LEGACY_DIR/assets" && chmod 777 "$LEGACY_DIR" "$LEGACY_DIR/assets"
if [ -n "$PREV_WEB_IMAGE" ]; then
docker run --rm --user 0 --entrypoint sh -v "$PWD/$LEGACY_DIR:/out" "$PREV_WEB_IMAGE" \
-c 'cp -a /usr/share/nginx/html/assets/. /out/assets/ 2>/dev/null || true' >/dev/null 2>&1 || true
echo " LEGACY_ASSETS=$(ls -1 "$LEGACY_DIR/assets" 2>/dev/null | tr '\n' ' ')"
fi
echo "=== 2. 构建 api"
docker build --network host "${PROXY_ARGS[@]}" "${IDENTITY_ARGS[@]}" -f Dockerfile -t knowledge-governance-p1-api:latest . || { echo "API_BUILD_FAILED"; exit 1; }
echo "=== 3. 构建 web"
docker build --network host "${PROXY_ARGS[@]}" -f Dockerfile.web -t knowledge-governance-p1-web:latest . || { echo "WEB_BUILD_FAILED"; exit 1; }
echo "=== 4. 上线 api + web(不动依赖)"
# COMPOSE_FILES 让调用方叠加发布接线 / 门禁 override,默认只有基础 compose.yaml。
# 例:COMPOSE_FILES="-f compose.yaml -f compose.release-sim.yaml" bash scripts/deploy-api-web.sh
#
# 注意(2026-10-10 实测踩到):重建 api 时若不叠加 compose.release-gates.yaml,两道生产门禁会被
# **静默关掉**(PRODUCTION_RELEASE_REQUESTS_ENABLED/BACKUP_RESTORE_VERIFIED=false),页面只在点击
# 「授权发布」时回一句 "production release requests are disabled" —— 很容易误判成功能坏了。
# 因此下面做前置保护:正在运行的 api 若已开闸、而本次没叠加门禁文件,直接拒绝(确要关闸用 ALLOW_GATE_OFF=1)。
COMPOSE_FILES_EFFECTIVE="${COMPOSE_FILES:--f compose.yaml}"
GATES_PREV=$(docker exec knowledge-governance-p1-api-1 sh -lc 'printf %s "${PRODUCTION_RELEASE_REQUESTS_ENABLED:-}"' 2>/dev/null || true)
if [ "${GATES_PREV}" = "true" ] && [[ "${COMPOSE_FILES_EFFECTIVE}" != *compose.release-gates.yaml* ]] && [ "${ALLOW_GATE_OFF:-0}" != "1" ]; then
echo "REFUSED_WOULD_CLOSE_PRODUCTION_GATES"
echo " 当前 api 已开闸(PRODUCTION_RELEASE_REQUESTS_ENABLED=true),而本次 COMPOSE_FILES 不含 compose.release-gates.yaml,"
echo " 继续会用基础 compose 重建 api → 生产授权接口被静默关闭。"
echo " 保留门禁:COMPOSE_FILES=\"-f compose.yaml -f compose.release-sim.yaml -f compose.release-gates.yaml\" bash scripts/deploy-api-web.sh"
echo " 确实要关闸:ALLOW_GATE_OFF=1 bash scripts/deploy-api-web.sh"
exit 1
fi
docker compose ${COMPOSE_FILES_EFFECTIVE} up -d --no-build --no-deps api web || { echo "UP_FAILED"; exit 1; }
echo "=== 5. 状态"
docker ps --filter name=knowledge-governance-p1 --format '{{.Names}} {{.Status}} {{.Image}}'
echo "api image=$(docker inspect -f '{{.Image}}' knowledge-governance-p1-api-1)"
echo "web image=$(docker inspect -f '{{.Image}}' knowledge-governance-p1-web-1)"
echo "=== 6. 健康"
echo "health=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8088/health)"
echo "=== 7. 生产端点环境变量(只报名字与非空)"
docker exec knowledge-governance-p1-api-1 sh -lc 'for v in PRODUCTION_WEKNORA_BASE_URL PRODUCTION_WEKNORA_API_KEY_FILE PRODUCTION_WEKNORA_ALLOW_DOCKER_HTTP; do eval "val=\$$v"; if [ -n "$val" ]; then echo "$v=<set>"; else echo "$v=<empty>"; fi; done'
echo "=== 7b. 生产门禁状态(重建后必须与预期一致)"
docker exec knowledge-governance-p1-api-1 sh -lc 'for v in PRODUCTION_RELEASE_REQUESTS_ENABLED PRODUCTION_BACKUP_RESTORE_VERIFIED; do eval "val=\$$v"; echo "$v=${val:-<empty>}"; done'
echo "=== 8. 新包"
docker exec knowledge-governance-p1-web-1 sh -lc 'grep -o "index-[A-Za-z0-9_-]*\.js" /usr/share/nginx/html/index.html | head -1'
echo "=== 9. 构建身份是否真的进了产物(页面据此判「当前运行」)"
INJECTED=$(printf '%s\n' "${IDENTITY_ARGS[@]}" | sed -n 's/^--build-arg=COMMIT_SHA=//p')
if docker exec knowledge-governance-p1-api-1 sh -lc "strings /app/api | grep -q '$INJECTED'"; then
echo "INJECTED_COMMIT_SHA=$INJECTED(已进 api 产物)"
else
echo "INJECTION_MISSING(产物里没有 $INJECTED —— 页面会显示当前运行未知)"
fi
echo "=== 10. 版本记录同步读回(当前 SHA + 注释快照 + 独立变更说明)"
# Startup queues the read-only history job. A healthy API alone is not proof.
VERIFIED=0
for ((attempt=0;attempt<90;attempt++)); do
if docker exec knowledge-governance-p1-api-1 /app/api --version-record-status; then VERIFIED=1; break; fi
sleep 2
done
[ "$VERIFIED" = "1" ] || { printf '%s\n' 'VERSION_RECORD_NOT_VERIFIED'; exit 1; }
echo "DONE"