013ce4a24e
build / build (push) Successful in 1m3s
收录 main、注释开发快照和完整运行 SHA;开发说明绑定原提交,补记 0.1.1–0.1.25。 进行中同步按仓库合并,完成后重新执行;启动入队,部署脚本读回版本证据。 开发快照独立于正式标签与 Release,既有冲突标签不移动。 已运行:整仓 make verify,Go 673 通过、前端 274 通过,92 个已声明可选跳过,必需跳过 0。 隔离 PostgreSQL 重复同步队列用例及 -race 通过;未更新现有 8088、未修改生产问答。
135 lines
8.2 KiB
Bash
135 lines
8.2 KiB
Bash
#!/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" |