diff --git a/.doc/ops/short-term/2026-09-27-test-single-binary.md b/.doc/ops/short-term/2026-09-27-test-single-binary.md new file mode 100644 index 00000000..83568ed3 --- /dev/null +++ b/.doc/ops/short-term/2026-09-27-test-single-binary.md @@ -0,0 +1,80 @@ +--- +title: "dev→test 晋升(rc-v1.1.0-beta.4)与 test 单二进制化" +task_id: test-single-binary-2026-09-27 +status: in_progress +created: 2026-09-27 +updated: 2026-09-27 +owner: pi +task_type: ops +task_tier: multi_stage +policy_revision: "AGENTS+worktree-ops @ 05d45022" +branch: feature/test-single-binary-2026-09-27 +worktree: .worktrees/test-single-binary-2026-09-27 +base_dev_sha: "05d45022b142a8a4c64a001618f6691093447f01" +write_scope: + - scripts/migrate-test-db.sh + - scripts/launch-single.sh + - .doc/ops/short-term/2026-09-27-test-single-binary.md + - .doc/test-reports/2026-09-27-test-single-binary-test-report.md + - .doc/test-reports/assets/2026-09-27-test-single-binary + - .doc/ops/CURRENT.md +allowed_environments: ["dev", "test"] +capabilities: [docs:publish, env:dev, env:test, git:dev, git:test] +mutex: env:dev+env:test+git:dev+git:test +review_state: pending +integration_state: pending +deployment: pending +cleanup: pending +remote_sync: not_requested +--- + +# dev→test 晋升与 test 单二进制化 + +## 用户目标与授权 + +- 用户目标:把 `dev` 合入 `test`;并将 test 环境转换为单二进制服务(与 dev 同形态)。 +- 授权来源:用户本会话明确指令(含 test 晋升与环境写操作)。未要求 push,远端同步不做。 +- 交付终点:test 分支 rc 晋升 + test 环境以单二进制形态运行并通过验收;任务记录、测试报告、CURRENT 收口。 + +## 背景与方案 + +- dev 已于 2026-09-24 完成单二进制化(localpg managed :5433 + PI sidecar :18865 + `launch.sh --no-docker`);设计文档 P6 即 test 部署演练。 +- 当前 test 仍为 4 容器(app/events/pi-agent/system-pg,schema 66);dev 领先 test 273 提交(dev schema 76)。 +- 方案(遵循设计 §4.2,PG 取 managed——单机测试机): + 1. 脚本落地:`scripts/migrate-test-db.sh`(复用参数化的 migrate-dev-db.sh,钉 test 默认值)+ `scripts/launch-single.sh`(非 dev 环境单二进制启动入口:SYSTEM_DB_*→CONTEXT_DB_* 映射、环境专属 sidecar、无 Vite)。 + 2. 脚本合入 dev 后跑全面门禁(arch-check / go vet / go test -short / npm test+build / scripts unittest)。 + 3. 真实 PI nightly(五例)@ dev tip → promotion receipt(PASS + digests)。 + 4. 晋升:任务 worktree 制备候选 M(P1=旧 test tip,`--no-ff` 合 dev tip)→ validate → `git:test` 租约内 update-ref + `rc-v1.1.0-beta.4` + candidate-images-apply test(compose 镜像 pin 保留为回滚路径)。 + 5. test 转换(`env:test` 租约,主目录执行):S0 快照 → `migrate-test-db.sh`(容器 dump → localpg :5434,PGDATA=data/test/pg,停 system-db 容器、volume 保留)→ 停 app/events/pi-agent 容器 → 从 rc tag `git archive` 构建 `bin/agent-server-test`(sha256 入报告)→ `launch-single.sh test`。 + 6. 验收:/health /ready(schema ≥76)、embed SPA :8081、PI sidecar health、test 真实 Runner 聊天链路、回滚路径说明。 +- 端口/数据/模型:8081 / 5434 / 18866;data/test;MiniMax-M2.7-highspeed(现行 test pin,configs/.env.test);SEED_DEMO_USERS=false。 + +## 非目标 + +- main 环境任何变更;FAV live 放量;push 远端;改 gap-matrix 门禁结论(仅按既有模式追加本轮证据区段)。 +- 前端/后端业务代码改动(如遇门禁失败按问题处理流程另立 fix 任务)。 + +## 相关问题与经验 + +- 问题台账/经验检索:「单二进制/localpg/no-docker」无直接命中;采纳 `LESSON-20260927`(隔离库配方、避开 5433/5434/5435 子串端口——隔离 gate-pg 用 5436;worktree 测试显式钉 DB)。 +- 遗留现场(本任务发现的待收口,非本任务造成):`integrate-upload-doc-access-2026-09-27` 仍 active,其 env:dev 租约有 recovery_required run(进程组含在运行的 dev agent-server PID 2636482,不能回收);dev server 正常服务中。处置:本任务收口时经 launch.sh 重启 dev server 脱离旧进程组后 recover 并关闭该任务,或登记给后续维护者。 + +## 步骤与验收 + +1. [x] 建 worktree / 注册 / CURRENT 认领 +2. [x] 脚本落地(migrate-test-db.sh / launch-single.sh) +3. [ ] worktree 验证(bash -n、scripts unittest、门禁套件) +4. [ ] 集成入 dev(候选 + validate + integrate) +5. [ ] dev tip 全面门禁 + nightly 真实 PI + receipt + candidate images +6. [ ] test 晋升(候选 M + rc-v1.1.0-beta.4) +7. [ ] test 单二进制转换 + 验收 +8. [ ] 报告 / CURRENT / 收口 + +## 验证与证据 + +见 [测试报告](../../test-reports/2026-09-27-test-single-binary-test-report.md)(执行后填写)。 + +## 收口与交接 + +- 分别记录 review_state / integration_state / deployment / cleanup / remote_sync。 +- 回滚(任一步失败且不可前滚时):容器路径 `docker compose -f docker-compose.test.yml -p agent-test up -d`(volumes 未删);DB 回滚用 S0 快照 restore 到容器 PG。 diff --git a/scripts/launch-single.sh b/scripts/launch-single.sh new file mode 100755 index 00000000..a49aa318 --- /dev/null +++ b/scripts/launch-single.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +# launch-single.sh — 非容器单二进制启动入口(非 dev 环境;当前用于 test) +# +# 与 launch.sh --no-docker(dev 专用:含 Vite/热更)相对,本脚本服务长期部署形态: +# - PI sidecar(环境专属端口/工作区)+ agent-server 单二进制(embed 静态,无 Vite) +# - LOCALPG_MODE 默认 managed(PGDATA=$DATA_DIR/pg,端口沿用环境约定) +# - 从 configs/.env. + 根 .env 分层加载配置;SYSTEM_DB_*(compose 约定) +# 在此映射为宿主运行所需 CONTEXT_DB_* +# +# 用法: +# ./scripts/launch-single.sh test [--bin /path/to/agent-server] [--build] +# --bin 指定二进制路径(默认 bin/agent-server-) +# --build 从当前工作树构建。test 部署应使用 rc tag 构建的制品,勿默认依赖 --build +# +# 日志:.tmp/launch/{server,pi-sidecar}-.log +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +ENV_NAME="" +if [[ $# -gt 0 && "${1:0:1}" != "-" ]]; then + ENV_NAME="$1" + shift +fi +BIN_OVERRIDE="" +DO_BUILD=0 +while [[ $# -gt 0 ]]; do + case "$1" in + --bin) BIN_OVERRIDE="${2:-}"; shift 2 ;; + --build) DO_BUILD=1; shift ;; + -h|--help) + sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//' + exit 0 + ;; + *) echo "unknown arg: $1" >&2; exit 1 ;; + esac +done + +if [[ -z "$ENV_NAME" || "$ENV_NAME" == "dev" ]]; then + echo "error: dev 请使用 ./scripts/launch.sh --no-docker;本脚本用于非 dev 环境(如 test)" >&2 + exit 1 +fi + +LOG_DIR="$ROOT/.tmp/launch" +mkdir -p "$LOG_DIR" + +info() { printf '\033[1;34m[launch-single]\033[0m %s\n' "$*"; } +ok() { printf '\033[1;32m ✅ %s\033[0m\n' "$*"; } +skip() { printf '\033[1;32m ⏭️ %s\033[0m\n' "$*"; } +act() { printf '\033[1;33m 🔄 %s\033[0m\n' "$*"; } +fail() { printf '\033[1;31m ❌ %s\033[0m\n' "$*"; } + +# ---------- 分层加载配置:根 .env(公共密钥)→ configs/.env.(环境差异) ---------- +# shellcheck disable=SC1091 +set -a +[[ -f "$ROOT/.env" ]] && . "$ROOT/.env" +if [[ -f "$ROOT/configs/.env.$ENV_NAME" ]]; then + . "$ROOT/configs/.env.$ENV_NAME" +else + fail "缺少 configs/.env.$ENV_NAME(参考 configs/.env.$ENV_NAME.example)" + exit 1 +fi +set +a + +# ---------- 宿主运行派生(进程环境变量优先于 .env 文件) ---------- +export ENV="$ENV_NAME" +DATA_DIR="${DATA_DIR:-$ROOT/data/$ENV_NAME}" +case "$DATA_DIR" in /*) ;; *) DATA_DIR="$ROOT/$DATA_DIR" ;; esac +export DATA_DIR + +HTTP_PORT="${HTTP_PORT:-8081}" +export HTTP_HOST="${HTTP_HOST:-${APP_PUBLISH_HOST:-0.0.0.0}}" +export HTTP_PORT +export SEED_DEMO_USERS="${SEED_DEMO_USERS:-false}" +export LOCALPG_MODE="${LOCALPG_MODE:-managed}" + +# SYSTEM_DB_*(compose 约定)→ CONTEXT_DB_*(config.Load 宿主读取约定) +export CONTEXT_DB_HOST="${CONTEXT_DB_HOST:-127.0.0.1}" +export CONTEXT_DB_PORT="${CONTEXT_DB_PORT:-${SYSTEM_DB_PUBLISH_PORT:-5434}}" +export CONTEXT_DB_USER="${CONTEXT_DB_USER:-${SYSTEM_DB_USER:-app}}" +export CONTEXT_DB_PASSWORD="${CONTEXT_DB_PASSWORD:-${SYSTEM_DB_PASSWORD:?SYSTEM_DB_PASSWORD required}}" +export CONTEXT_DB_NAME="${CONTEXT_DB_NAME:-${SYSTEM_DB_NAME:-agent_system_$ENV_NAME}}" + +PI_PORT="${PI_AGENT_RUNNER_PORT:-18866}" +export PI_AGENT_RUNNER_URL="${PI_AGENT_RUNNER_URL:-http://127.0.0.1:$PI_PORT}" + +# 与 compose 对齐的 vault 状态根(FAV 关闭时 inert;无 master key 时 health 同样报 disconnected) +export VAULT_STATE_ROOT="${VAULT_STATE_ROOT:-$ROOT/data/vault/$ENV_NAME}" +export VAULT_STORAGE_IDENTITY="${VAULT_STORAGE_IDENTITY:-${ENV_NAME}-vault}" + +BIN="${BIN_OVERRIDE:-$ROOT/bin/agent-server-$ENV_NAME}" + +wait_http() { + local url="$1" retries="${2:-30}" i + for ((i = 0; i < retries; i++)); do + curl -sf -m 2 "$url" >/dev/null 2>&1 && return 0 + sleep 1 + done + return 1 +} + +stop_pid_on_port() { + local port="$1" pid + pid="$(lsof -nP -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | head -n1 || true)" + if [[ -n "$pid" ]]; then + act "停止占用 :$port 的进程 PID $pid" + kill "$pid" 2>/dev/null || true + for ((i = 0; i < 10; i++)); do + lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1 || return 0 + sleep 1 + done + lsof -nP -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | xargs -r kill -9 2>/dev/null || true + fi +} + +info "环境: $ENV_NAME(HTTP :$HTTP_HOST:$HTTP_PORT / DB :$CONTEXT_DB_PORT/$CONTEXT_DB_NAME / PI :$PI_PORT / DATA_DIR=$DATA_DIR)" + +# ---------- PI sidecar ---------- +info "检测 PI sidecar :$PI_PORT ..." +if wait_http "http://127.0.0.1:$PI_PORT/health" 2; then + skip "PI sidecar 已健康" +else + stop_pid_on_port "$PI_PORT" + act "启动 PI sidecar(日志 $LOG_DIR/pi-sidecar-$ENV_NAME.log)..." + ( + export PORT="$PI_PORT" + export DATA_DIR="$DATA_DIR/workspaces" + export PI_CODING_AGENT_DIR="$DATA_DIR/pi-agent" + nohup bash "$ROOT/deploy/pi-agent-sidecar/start.sh" >"$LOG_DIR/pi-sidecar-$ENV_NAME.log" 2>&1 & + ) + if wait_http "http://127.0.0.1:$PI_PORT/health" 60; then + ok "PI sidecar 就绪" + else + fail "PI sidecar 健康检查超时,见 $LOG_DIR/pi-sidecar-$ENV_NAME.log" + exit 1 + fi +fi + +# ---------- agent-server ---------- +if [[ "$DO_BUILD" -eq 1 ]]; then + act "从当前工作树构建 $BIN ..." + OUT_BIN="$BIN" bash "$ROOT/scripts/build-single-binary.sh" || { fail "构建失败"; exit 1; } +fi +if [[ ! -x "$BIN" ]]; then + fail "二进制不存在或不可执行: $BIN(用 --bin 指定 rc tag 构建的制品,或 --build)" + exit 1 +fi + +stop_pid_on_port "$HTTP_PORT" +act "后台启动 agent-server(LOCALPG_MODE=$LOCALPG_MODE,日志 $LOG_DIR/server-$ENV_NAME.log)..." +( + cd "$ROOT" + nohup "$BIN" >"$LOG_DIR/server-$ENV_NAME.log" 2>&1 & +) +if wait_http "http://127.0.0.1:$HTTP_PORT/api/v1/health" 120; then + ok "后端已启动并通过健康检查" +else + fail "后端健康检查超时,见 $LOG_DIR/server-$ENV_NAME.log" + exit 1 +fi + +info "完成。入口: http://127.0.0.1:$HTTP_PORT PI: http://127.0.0.1:$PI_PORT" +info "若首次从容器切换,请先跑: ./scripts/migrate-test-db.sh(test),容器 volume 保留可回滚" diff --git a/scripts/migrate-test-db.sh b/scripts/migrate-test-db.sh new file mode 100755 index 00000000..50b10626 --- /dev/null +++ b/scripts/migrate-test-db.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# migrate-test-db.sh — dump 容器 system-db (shuimu-system-pg-test :5434) → 本机 localpg :5434 +# +# 复用 migrate-dev-db.sh 的通用逻辑(其全部差异点已参数化);本脚本只钉 test 默认值: +# CONTAINER=shuimu-system-pg-test DATA_DIR=data/test PORT=5434 DB=agent_system_test +# 前置: +# - 容器 shuimu-system-pg-test 健康(作为 dump 源) +# - brew postgresql@18 + pgvector 已安装(LOCALPG_BIN 可覆盖) +# - configs/.env.test 提供 SYSTEM_DB_*(或用环境变量覆盖) +# 不删除 docker volume;回滚:docker compose -f docker-compose.test.yml -p agent-test up -d system-db +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# 读取 test 环境 SSOT(SYSTEM_DB_* 等;含密钥,勿回显) +# shellcheck disable=SC1091 +if [[ -f "$ROOT/configs/.env.test" ]]; then + set -a + . "$ROOT/configs/.env.test" + set +a +fi + +export CONTAINER="${DEV_PG_CONTAINER:-shuimu-system-pg-test}" +export DATA_DIR="${MIGRATE_DATA_DIR:-$ROOT/data/test}" # 绝对路径,避免 PGDATA 相对解析漂移 +export PORT="${CONTEXT_DB_PORT:-${SYSTEM_DB_PUBLISH_PORT:-5434}}" +export CONTEXT_DB_PORT="$PORT" +export DB_USER="${CONTEXT_DB_USER:-${SYSTEM_DB_USER:-app}}" +export DB_PASS="${CONTEXT_DB_PASSWORD:-${SYSTEM_DB_PASSWORD:?SYSTEM_DB_PASSWORD required (configs/.env.test)}}" +export DB_NAME="${CONTEXT_DB_NAME:-${SYSTEM_DB_NAME:-agent_system_test}}" +export DUMP="${DUMP_PATH:-$ROOT/.tmp/test-pg-migrate.dump}" +# migrate-dev-db.sh 的 dump 步骤读取 SYSTEM_DB_USER / SYSTEM_DB_NAME +export SYSTEM_DB_USER="${SYSTEM_DB_USER:-app}" +export SYSTEM_DB_NAME="${SYSTEM_DB_NAME:-agent_system_test}" + +bash "$ROOT/scripts/migrate-dev-db.sh"