Merge authoritative main: take the newer free-port test helpers
This commit is contained in:
@@ -84,7 +84,7 @@ start_serve() {
|
||||
stop_serve() { [ -n "${API_PID:-}" ] && { kill "$API_PID" 2>/dev/null || true; wait "$API_PID" 2>/dev/null || true; API_PID=""; }; }
|
||||
cleanup() {
|
||||
stop_serve
|
||||
if [ -n "${UI_PID:-}" ] && ps -p "$UI_PID" -o args= 2>/dev/null | grep -Fq -- "vite preview --port $UI_PORT"; then
|
||||
if [ -n "${UI_PID:-}" ] && grep -Fq -- "vite preview --port $UI_PORT" < <(ps -p "$UI_PID" -o args= 2>/dev/null); then
|
||||
kill "$UI_PID" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
@@ -125,7 +125,7 @@ cmd() { # cmd <jar> <id> <params> → payload;rejected 即死
|
||||
|| die "command $2 rejected"
|
||||
}
|
||||
log "seed: host project + members + doc + duties + grant"
|
||||
log "me probe: $(curl -s -b "$AJ" "$API_BASE/api/me" | head -c 160) | jar=$(ls -la "$AJ" 2>/dev/null | awk '{print $5}')"
|
||||
log "me probe: $(head -c 160 < <(curl -s -b "$AJ" "$API_BASE/api/me")) | jar=$(ls -la "$AJ" 2>/dev/null | awk '{print $5}')"
|
||||
PIDRESP=$(curl -s -w '\n%{http_code}' -b "$AJ" -X POST "$API_BASE/api/projects" -H 'Content-Type: application/json' -d '{"name":"职责验收夹具"}')
|
||||
CODE=${PIDRESP##*$'\n'}; BODY=${PIDRESP%$'\n'*}
|
||||
[ "$CODE" = 200 ] || die "host project create: HTTP $CODE body=${BODY:0:200}"
|
||||
|
||||
+1
-1
@@ -208,7 +208,7 @@ log "seed: load cockpit doc-tree into $PID_MAIN"
|
||||
"$OUT/agent-system-rdm" -data "$DATA" \
|
||||
-structure "$DEV_ROOT/docs/structure/cockpit-e2e.yaml" -project "$PID_MAIN" \
|
||||
>"$OUT/structure-cockpit.log" 2>&1 || { tail -5 "$OUT/structure-cockpit.log"; die "cockpit structure load failed"; }
|
||||
grep -o '装载(.*)' "$OUT/structure.log" | head -1 || true
|
||||
grep -m1 -o '装载(.*)' "$OUT/structure.log" || true
|
||||
|
||||
# 阶段③:重启服务,切片入库挂载到已装载骨架(API)。
|
||||
start_serve
|
||||
|
||||
@@ -6,7 +6,7 @@ case "$branch" in
|
||||
echo "pre-commit: 拒绝——集成分支 ${branch} 不得直接提交,走 feature 分支 + PR(collaboration.md)" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
echo "$branch" | grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' || {
|
||||
grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' <<<"$branch" || {
|
||||
echo "pre-commit: 拒绝——分支名须为 feature|fix|hotfix|release/<slug>,当前:${branch}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ for dir in deps/agent-intent deps/agent-native deps/agent-squad; do
|
||||
deps_out=$(cd "$root/$dir" && GOWORK=off go list -deps ./...) || fail=1
|
||||
for sib in "${siblings[@]}"; do
|
||||
[ "$sib" = "$mod" ] && continue
|
||||
if printf '%s\n' "$deps_out" | grep -E -q "^(${sib}|${sib}/)"; then
|
||||
if grep -E -q "^(${sib}|${sib}/)" <<<"$deps_out"; then
|
||||
echo "boundary: $mod core depends on $sib" >&2
|
||||
fail=1
|
||||
fi
|
||||
|
||||
+124
-34
@@ -897,13 +897,13 @@ derive_port() {
|
||||
# svc_get KEY:读当前服务 svc.env 的键值(无则空)。
|
||||
svc_get() {
|
||||
[ -f "$ENV_DIR/svc.env" ] || return 0
|
||||
sed -n "s/^$1=//p" "$ENV_DIR/svc.env" | head -1
|
||||
sed -n "/^$1=/{s///p;q;}" "$ENV_DIR/svc.env"
|
||||
}
|
||||
|
||||
# svc_kind NAME:服务类型(svc.env kind=,无登记或无 kind 键 → agent,兼容旧登记)。
|
||||
svc_kind() {
|
||||
local k=""
|
||||
[ -f "$RUNTIME/$1/svc.env" ] && k="$(sed -n 's/^kind=//p' "$RUNTIME/$1/svc.env" | head -1)"
|
||||
[ -f "$RUNTIME/$1/svc.env" ] && k="$(sed -n '/^kind=/{s///p;q;}' "$RUNTIME/$1/svc.env")"
|
||||
[ -n "$k" ] && printf '%s' "$k" || printf agent
|
||||
}
|
||||
|
||||
@@ -942,6 +942,7 @@ svc_merge_tags() {
|
||||
# (托管绑定不重复存 DSN,外部直连存受保护 dsn=);未走解析的调用保持旧值。
|
||||
record_state() {
|
||||
local pid prev_src prev_dbref prev_conffile prev_db prev_dsn prev_envfile prev_plugins plugins_out merged_tags
|
||||
local prev_saenv
|
||||
pid="$(read_pid)"
|
||||
prev_src=""
|
||||
[ -f "$ENV_DIR/svc.env" ] && prev_src="$(svc_get source)"
|
||||
@@ -950,6 +951,7 @@ record_state() {
|
||||
prev_db="$(svc_get db)"
|
||||
prev_dsn="$(svc_get dsn)"
|
||||
prev_envfile="$(svc_get envfile)"
|
||||
prev_saenv="$(svc_get superadmin_envfile)"
|
||||
prev_plugins="$(svc_get plugins)"
|
||||
plugins_out="$prev_plugins"
|
||||
# 只有激活校验和 UI 探活都通过之后才提交新的 plugins=。中途 record_state
|
||||
@@ -999,6 +1001,11 @@ record_state() {
|
||||
elif [ -n "$prev_envfile" ]; then
|
||||
printf 'envfile=%s\n' "$prev_envfile"
|
||||
fi
|
||||
if [ -n "${SUPERADMIN_ENVFILE_NEW:-}" ]; then
|
||||
printf 'superadmin_envfile=%s\n' "$SUPERADMIN_ENVFILE_NEW"
|
||||
elif [ "${SUPERADMIN_ENVFILE_CLEAR:-0}" != 1 ] && [ -n "$prev_saenv" ]; then
|
||||
printf 'superadmin_envfile=%s\n' "$prev_saenv"
|
||||
fi
|
||||
[ -n "$plugins_out" ] && printf 'plugins=%s\n' "$plugins_out"
|
||||
[ $# -gt 0 ] && printf '%s\n' "$@"
|
||||
} > "$tmp"
|
||||
@@ -1055,7 +1062,7 @@ proc_stop() {
|
||||
port_owner() {
|
||||
local port="${ENV_ADDR##*:}"
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
ss -ltnp 2>/dev/null | awk -v p=":$port" '$4 ~ p"$" {print $0; exit}'
|
||||
awk -v p=":$port" '$4 ~ p"$" {print $0; exit}' < <(ss -ltnp 2>/dev/null)
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -1064,7 +1071,7 @@ port_owner_pid() {
|
||||
local line pid
|
||||
line="$(port_owner)"
|
||||
[ -n "$line" ] || return 1
|
||||
pid="$(printf '%s' "$line" | sed -n 's/.*pid=\([0-9][0-9]*\).*/\1/p' | head -1)"
|
||||
pid="$(sed -n '/.*pid=\([0-9][0-9]*\).*/{s//\1/p;q;}' <<<"$line")"
|
||||
[ -n "$pid" ] || return 1
|
||||
printf '%s' "$pid"
|
||||
}
|
||||
@@ -1355,6 +1362,8 @@ svc_identity_run() {
|
||||
svc_db_identity_ensure() {
|
||||
# 仅 fresh/pending 或显式 adopt 调用。established 缺行不得走这里自动补。
|
||||
# 非 agent 或无能力:跳过。可选参数 BIN:部署候选。
|
||||
# SVC_IDENTITY_LAST_OUT:最近一次 identity CLI 输出,供候选门禁区分失败原因。
|
||||
SVC_IDENTITY_LAST_OUT=""
|
||||
local bin="${1:-${ENV_BIN:-}}"
|
||||
manifest_ok || return 0
|
||||
local kind; kind="$(svc_get kind)"
|
||||
@@ -1365,6 +1374,7 @@ svc_db_identity_ensure() {
|
||||
IFS=$'\t' read -r iid dbref data <<<"$line"
|
||||
local out rc=0
|
||||
out="$(svc_identity_run "$bin" ensure "$iid" "$dbref" "$data" 2>&1)" || rc=$?
|
||||
SVC_IDENTITY_LAST_OUT="$out"
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
c_err "[$ENV_NAME] DB 身份 ensure 失败:$out"
|
||||
return 1
|
||||
@@ -1374,6 +1384,7 @@ svc_db_identity_ensure() {
|
||||
}
|
||||
|
||||
svc_db_identity_verify() {
|
||||
SVC_IDENTITY_LAST_OUT=""
|
||||
manifest_ok || return 0
|
||||
local kind; kind="$(svc_get kind)"
|
||||
[ "$kind" = agent ] || return 0
|
||||
@@ -1384,6 +1395,7 @@ svc_db_identity_verify() {
|
||||
IFS=$'\t' read -r iid dbref data <<<"$line"
|
||||
local out rc=0
|
||||
out="$(svc_identity_run "$bin" verify "$iid" "$dbref" "$data" 2>&1)" || rc=$?
|
||||
SVC_IDENTITY_LAST_OUT="$out"
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
c_err "[$ENV_NAME] DB 身份校验失败,拒绝启动:$out"
|
||||
c_err " 先调查库文件与身份行;确认归属后 scripts/svc.sh $ENV_NAME adopt --confirm $iid"
|
||||
@@ -1395,6 +1407,22 @@ svc_db_identity_verify() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# svc_identity_schema_missing SRCBIN:最近一次 identity CLI 报身份表缺失(库 schema
|
||||
# 早于 v27)时打印迁移指引并返回 0;否则返回 1,调用方沿用原失败提示。不自动迁移。
|
||||
svc_identity_schema_missing() {
|
||||
case "${SVC_IDENTITY_LAST_OUT:-}" in
|
||||
*"identity table missing"*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
local data_root
|
||||
data_root="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["identity"]["data_root"])' "$ENV_DIR/manifest.json" 2>/dev/null)" \
|
||||
|| data_root="<data_root>"
|
||||
c_err "[$ENV_NAME] 数据库 schema 早于身份表(pre-v27,identity table missing):候选无法补齐/验证 DB 身份;旧服务/旧制品/登记保持不变"
|
||||
c_err " 处理:先备份数据库 → scripts/svc.sh $ENV_NAME stop → 用候选制品执行 $1 migrate --data $data_root(PostgreSQL:DSN 只经环境变量 AGENT_SYSTEM_DSN 传入,不进 argv)→ 再次部署"
|
||||
c_err " svc 不会自动迁移;本次暂存的候选在拒绝后清理,迁移请使用同一版本的制品"
|
||||
return 0
|
||||
}
|
||||
|
||||
# svc_deploy_identity_gate SRCBIN:部署候选的身份门禁——在停旧服务、覆盖制品之前
|
||||
# 执行(2026-10-12 rdm1/rdmcand 事故:停服→覆盖→启动即被身份门禁拒绝,只剩
|
||||
# 「登记版本≠磁盘制品」的混合状态且无恢复路径)。规则:
|
||||
@@ -1425,7 +1453,8 @@ svc_deploy_identity_gate() {
|
||||
data_root="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["identity"]["data_root"])' "$ENV_DIR/manifest.json")"
|
||||
if [ -f "$data_root/agent-system.db" ] || { [ "${DB_MODE:-}" = pg ] && [ -n "${DB_DSN:-}" ]; }; then
|
||||
svc_db_identity_ensure "$srcbin" || {
|
||||
c_err "[$ENV_NAME] 候选补齐 DB 身份失败(外宾行/不一致 fail-closed);旧服务/旧制品/登记保持不变"
|
||||
svc_identity_schema_missing "$srcbin" \
|
||||
|| c_err "[$ENV_NAME] 候选补齐 DB 身份失败(外宾行/不一致 fail-closed);旧服务/旧制品/登记保持不变"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
@@ -1433,7 +1462,8 @@ svc_deploy_identity_gate() {
|
||||
*)
|
||||
# established:只验证,不补行。缺行或不匹配在停旧服务前拒绝。
|
||||
svc_db_identity_verify "$srcbin" || {
|
||||
c_err "[$ENV_NAME] 候选无法验证已建立的 DB 身份;旧服务/旧制品/登记保持不变"
|
||||
svc_identity_schema_missing "$srcbin" \
|
||||
|| c_err "[$ENV_NAME] 候选无法验证已建立的 DB 身份;旧服务/旧制品/登记保持不变"
|
||||
return 1
|
||||
}
|
||||
;;
|
||||
@@ -1473,7 +1503,7 @@ PY
|
||||
iid="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["instance_id"])' "$ENV_DIR/manifest.json")"
|
||||
[ "$CLI_CONFIRM" = "$iid" ] || { c_err "[$ENV_NAME] --migrate-schema 需要 --confirm $iid"; return 1; }
|
||||
backup="$(agent_backup_root)"
|
||||
if ! find "$backup" -mindepth 1 -maxdepth 1 -type d -print -quit | grep -q .; then
|
||||
if ! grep -q . < <(find "$backup" -mindepth 1 -maxdepth 1 -type d -print -quit); then
|
||||
c_err "[$ENV_NAME] forward-only 迁移必须先 backup"
|
||||
return 1
|
||||
fi
|
||||
@@ -1506,6 +1536,38 @@ svc_write_serve_env() {
|
||||
chmod 0600 "$f" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# svc_superadmin_envfile_check:agent 的 start/restart/deploy 在停旧服务前校验本次
|
||||
# AGENT_SYSTEM_ENV_FILE(只看路径与元数据,绝不读内容)。非空须为当前用户所有、
|
||||
# 无组/其他权限位的普通文件,通过后由 record_state 记为 superadmin_envfile=;
|
||||
# 已设置但为空则清除登记。
|
||||
SUPERADMIN_ENVFILE_NEW=""
|
||||
SUPERADMIN_ENVFILE_CLEAR=0
|
||||
svc_superadmin_envfile_check() {
|
||||
[ -n "${AGENT_SYSTEM_ENV_FILE+x}" ] || return 0
|
||||
if [ -z "$AGENT_SYSTEM_ENV_FILE" ]; then
|
||||
SUPERADMIN_ENVFILE_CLEAR=1
|
||||
return 0
|
||||
fi
|
||||
local f="$AGENT_SYSTEM_ENV_FILE" dir meta owner mode
|
||||
dir="$(cd "$(dirname "$f")" 2>/dev/null && pwd)" || dir=""
|
||||
[ -n "$dir" ] && f="${dir%/}/$(basename "$f")"
|
||||
if [ -z "$dir" ] || [ ! -f "$f" ]; then
|
||||
c_err "[$ENV_NAME] 超级管理员凭据文件不存在或不是普通文件:$f(运行中的服务未改动)"
|
||||
return 1
|
||||
fi
|
||||
meta="$(stat -L -c '%u %a' "$f" 2>/dev/null)" || meta=""
|
||||
owner="${meta%% *}"; mode="${meta##* }"
|
||||
if [ -z "$meta" ] || [ "$owner" != "$(id -u)" ]; then
|
||||
c_err "[$ENV_NAME] 超级管理员凭据文件不属于当前用户:$f(运行中的服务未改动)"
|
||||
return 1
|
||||
fi
|
||||
if [ $(( 8#$mode & 8#077 )) -ne 0 ]; then
|
||||
c_err "[$ENV_NAME] 超级管理员凭据文件权限过宽($mode):$f,须 chmod 600(运行中的服务未改动)"
|
||||
return 1
|
||||
fi
|
||||
SUPERADMIN_ENVFILE_NEW="$f"
|
||||
}
|
||||
|
||||
# svc_launch_serve ARGS:env -i 只带 serve.env 与本次构造的 guard_env。
|
||||
svc_launch_serve() {
|
||||
local exec_args="$1"
|
||||
@@ -1535,11 +1597,25 @@ svc_launch_serve() {
|
||||
args+=("RDM_DISABLE_EXEC_TOOLS=$RDM_DISABLE_EXEC_TOOLS")
|
||||
fi
|
||||
# 超级管理员凭据只从本次环境转发,不写入 serve.env;缺失时由 serve 自行 fail closed。
|
||||
# 本次环境三者皆未设置时回落到 svc.env 登记的 superadmin_envfile=(只转发路径),
|
||||
# recover/watchdog 的清空环境子进程因此也能拉起认证实例。
|
||||
local sa_explicit=0 sa_file
|
||||
for k in SUPER_ADMIN_OWNER SUPER_ADMIN_PASSWORD AGENT_SYSTEM_ENV_FILE; do
|
||||
if [ -n "${!k+x}" ]; then
|
||||
args+=("$k=${!k}")
|
||||
sa_explicit=1
|
||||
fi
|
||||
done
|
||||
if [ "$sa_explicit" = 0 ]; then
|
||||
sa_file="$(svc_get superadmin_envfile)"
|
||||
if [ -n "$sa_file" ]; then
|
||||
if [ -f "$sa_file" ]; then
|
||||
args+=("AGENT_SYSTEM_ENV_FILE=$sa_file")
|
||||
else
|
||||
c_warn "[$ENV_NAME] 登记的超级管理员凭据文件已不存在:$sa_file(不转发,serve 将按缺凭据拒启)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
# shellcheck disable=SC2086
|
||||
nohup "${args[@]}" "$ENV_BIN" serve $exec_args >> "$ENV_DIR/svc.log" 2>&1 9>&- &
|
||||
}
|
||||
@@ -1700,7 +1776,7 @@ adjust_args_for_bin() {
|
||||
case "$w" in
|
||||
--auth|--config)
|
||||
flag="${w#--}"
|
||||
if printf '%s\n' "$out" | grep -Eq -- "(^|[[:space:]])-{1,2}${flag}([[:space:]]|=|$)"; then
|
||||
if grep -Eq -- "(^|[[:space:]])-{1,2}${flag}([[:space:]]|=|$)" <<<"$out"; then
|
||||
filtered="$filtered $w"
|
||||
else
|
||||
drop_next=1
|
||||
@@ -2429,7 +2505,7 @@ materialize_release() {
|
||||
fi
|
||||
if [ -f "$mf" ]; then
|
||||
local want have
|
||||
want="$(sed -n 's/.*"agent-system": *"\([0-9a-f]\{64\}\)".*/\1/p' "$mf" | head -1)"
|
||||
want="$(sed -n '/.*"agent-system": *"\([0-9a-f]\{64\}\)".*/{s//\1/p;q;}' "$mf")"
|
||||
have="$(sha256sum "$bin" | cut -d' ' -f1)"
|
||||
if [ -n "$want" ] && [ "$want" != "$have" ]; then
|
||||
c_err "[$ENV_NAME] sha256 校验失败:制品与 manifest 不符($bin)——拒绝部署"
|
||||
@@ -3317,7 +3393,7 @@ agent_do_deploy() {
|
||||
fi
|
||||
local mf="$REL_ROOT/versions/$REL_VERSION/manifest.json" want have
|
||||
if [ -f "$mf" ]; then
|
||||
want="$(sed -n 's/.*"agent-system": *"\([0-9a-f]\{64\}\)".*/\1/p' "$mf" | head -1)"
|
||||
want="$(sed -n '/.*"agent-system": *"\([0-9a-f]\{64\}\)".*/{s//\1/p;q;}' "$mf")"
|
||||
have="$(sha256sum "$relbin" | cut -d' ' -f1)"
|
||||
if [ -n "$want" ] && [ "$want" != "$have" ]; then
|
||||
c_err "[$ENV_NAME] sha256 校验失败:制品与 manifest 不符($relbin)——拒绝部署"
|
||||
@@ -3574,7 +3650,7 @@ for u in users:
|
||||
"$ENV_BIN" apikey revoke ${seed_dbf[@]+"${seed_dbf[@]}"} --owner "$seed_owner" --name svc-seed >/dev/null 2>&1 \
|
||||
|| c_warn "[$ENV_NAME] 临时播种密钥回收失败:请手动吊销 owner=$seed_owner name=svc-seed"
|
||||
fi
|
||||
if echo "$resp" | grep -q '"ok": *true'; then
|
||||
if grep -q '"ok": *true' <<<"$resp"; then
|
||||
local did=""
|
||||
[ -n "${CLI_MBASE:-${SVC_MODEL_BASE:-}}${CLI_MKEY:-${SVC_MODEL_KEY:-}}${CLI_MNAME:-${SVC_MODEL_NAME:-}}" ] && did="主模型"
|
||||
[ -n "${CLI_LBASE:-${SVC_LIGHT_BASE:-}}${CLI_LKEY:-${SVC_LIGHT_KEY:-}}${CLI_LNAME:-${SVC_LIGHT_NAME:-}}" ] && did="${did:+$did、}轻量模型"
|
||||
@@ -3801,7 +3877,7 @@ resolve_db_config() {
|
||||
# conffile 的 dsn 键(二进制自行读文件,连接串不经环境重复传递)。
|
||||
local cf_dsn=""
|
||||
if [ -n "$p_conf" ] && [ -f "$p_conf" ]; then
|
||||
cf_dsn="$(grep -oE '"dsn"[[:space:]]*:[[:space:]]*"[^"]*"' "$p_conf" 2>/dev/null | head -1 | sed -E 's/.*"([^"]*)"$/\1/')"
|
||||
cf_dsn="$(sed -n '/"dsn"[[:space:]]*:[[:space:]]*"[^"]*"/{s//\n&/;s/.*\n"dsn"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p;q;}' "$p_conf" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
# mock 档与数据库配置共存 = 冲突,拒绝(行为要明确,不隐式降级)。
|
||||
@@ -3940,7 +4016,7 @@ bin_env_dsn_supported() {
|
||||
# 「支持」(回归教训:test_svc 的 legacy 桩即脚本形态)。
|
||||
[ "$(head -c 4 "$1" 2>/dev/null)" = $'\x7fELF' ] || return 1
|
||||
command -v strings >/dev/null 2>&1 || return 1
|
||||
strings "$1" 2>/dev/null | grep -q 'AGENT_SYSTEM_DSN' && return 0
|
||||
grep -q 'AGENT_SYSTEM_DSN' < <(strings "$1" 2>/dev/null) && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -4357,7 +4433,7 @@ db_create() {
|
||||
SVC_KIND=postgres
|
||||
SRC_LABEL="postgres@local"
|
||||
local pgver merged_tags
|
||||
pgver="$("$pgbin/postgres" --version 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i ~ /^[0-9]+(\.[0-9]+)+$/) {print $i; exit}}' || true)"
|
||||
pgver="$(awk '{for(i=1;i<=NF;i++) if ($i ~ /^[0-9]+(\.[0-9]+)+$/) {print $i; exit}}' < <("$pgbin/postgres" --version 2>/dev/null) || true)"
|
||||
merged_tags="$(svc_merge_tags)"
|
||||
{
|
||||
printf 'kind=postgres\n'
|
||||
@@ -4412,7 +4488,7 @@ pg_do_start() {
|
||||
# 首次启动:建默认库(已存在则跳过)。
|
||||
local dbname; dbname="$(svc_get dbname)"
|
||||
if [ -n "$dbname" ]; then
|
||||
if ! pg_psql "SELECT 1 FROM pg_database WHERE datname='$dbname'" tA | grep -q 1; then
|
||||
if ! grep -q 1 < <(pg_psql "SELECT 1 FROM pg_database WHERE datname='$dbname'" tA); then
|
||||
if pg_psql "CREATE DATABASE \"$dbname\""; then
|
||||
c_ok "[$ENV_NAME] 已建库:$dbname"
|
||||
else
|
||||
@@ -4920,7 +4996,7 @@ EOF
|
||||
[ -n "$target" ] && { c_err "--clear 不接受文件参数"; return 2; }
|
||||
local exec_old new_exec
|
||||
exec_old="$(svc_get exec)"
|
||||
if printf '%s' "$exec_old" | grep -q -- '--config '; then
|
||||
if grep -q -- '--config ' <<<"$exec_old"; then
|
||||
new_exec="$(printf '%s' "$exec_old" | sed -E 's/(^| )--config [^ ]+//; s/ +/ /g; s/^ //; s/ $//')"
|
||||
svc_set_key exec "$new_exec"
|
||||
fi
|
||||
@@ -4946,13 +5022,13 @@ EOF
|
||||
c_err "(键与取值以 config explain 为准;先修正文件再切换)"
|
||||
return 2
|
||||
fi
|
||||
if ! "$ENV_BIN" serve -h 2>&1 | grep -Eq -- "(^|[[:space:]])-{1,2}config([[:space:]]|=|$)"; then
|
||||
if ! grep -Eq -- "(^|[[:space:]])-{1,2}config([[:space:]]|=|$)" < <("$ENV_BIN" serve -h 2>&1); then
|
||||
c_warn "[$ENV_NAME] 当前产物不识别 --config(旧版本二进制)——绑定已登记,但重启将忽略该参数"
|
||||
fi
|
||||
fi
|
||||
local exec_old new_exec
|
||||
exec_old="$(svc_get exec)"
|
||||
if printf '%s' "$exec_old" | grep -q -- '--config '; then
|
||||
if grep -q -- '--config ' <<<"$exec_old"; then
|
||||
new_exec="$(printf '%s' "$exec_old" | sed -E "s|--config [^ ]+|--config $abs|")"
|
||||
else
|
||||
new_exec="${exec_old:+$exec_old }--config $abs"
|
||||
@@ -5037,7 +5113,7 @@ EOF
|
||||
[ -n "$target" ] && { c_err "--clear 不接受服务名参数"; return 2; }
|
||||
local exec_old new_exec
|
||||
exec_old="$(svc_get exec)"
|
||||
if printf '%s' "$exec_old" | grep -q -- '--dsn '; then
|
||||
if grep -q -- '--dsn ' <<<"$exec_old"; then
|
||||
new_exec="$(printf '%s' "$exec_old" | sed -E 's/(^| )--dsn [^ ]+//; s/ +/ /g; s/^ //; s/ $//')"
|
||||
svc_set_key exec "$new_exec"
|
||||
fi
|
||||
@@ -5084,7 +5160,7 @@ EOF
|
||||
# 实例运行中:确保目标库存在(缺则建,幂等)。
|
||||
st="$(svc_pid_state "$target")"
|
||||
if [ "${st%% *}" = alive ]; then
|
||||
if ! pg_run "$thost" "$tport" "$tuser" "$tpass" postgres "SELECT 1 FROM pg_database WHERE datname='$tdb'" tA | grep -q 1; then
|
||||
if ! grep -q 1 < <(pg_run "$thost" "$tport" "$tuser" "$tpass" postgres "SELECT 1 FROM pg_database WHERE datname='$tdb'" tA); then
|
||||
if pg_run "$thost" "$tport" "$tuser" "$tpass" postgres "CREATE DATABASE \"$tdb\"" >/dev/null; then
|
||||
c_ok "[$target] 已建库:$tdb"
|
||||
else
|
||||
@@ -5097,7 +5173,7 @@ EOF
|
||||
dsn="postgresql://$tuser:$tpass@$thost:$tport/$tdb"
|
||||
# 写持久化配置(托管绑定形态):dbref= + db=pg;清除直接 dsn=;剥离 exec 残留 --dsn。
|
||||
# DSN 不落 exec=——启动时由实例登记重建(实例端口/密码变更自动跟随,密码不重复保存)。
|
||||
if printf '%s' "$exec_now" | grep -q -- '--dsn '; then
|
||||
if grep -q -- '--dsn ' <<<"$exec_now"; then
|
||||
local new_exec
|
||||
new_exec="$(printf '%s' "$exec_now" | sed -E 's/(^| )--dsn [^ ]+//; s/ +/ /g; s/^ //; s/ $//')"
|
||||
svc_set_key exec "$new_exec"
|
||||
@@ -5153,7 +5229,7 @@ agent_backup_snapshot() { # $1=dest:svc.env + history.jsonl + data/(*.db 一
|
||||
[ -f "$ENV_DATA/$rel" ] || continue
|
||||
case "$rel" in
|
||||
*"'"*|*'"'*)
|
||||
if head -c 15 "$ENV_DATA/$rel" | grep -qa '^SQLite format 3$'; then
|
||||
if grep -qa '^SQLite format 3$' < <(head -c 15 "$ENV_DATA/$rel"); then
|
||||
c_err "[$ENV_NAME] 路径含引号,无法走 sqlite 一致性导出:$rel"
|
||||
return 1
|
||||
fi
|
||||
@@ -5166,7 +5242,7 @@ agent_backup_snapshot() { # $1=dest:svc.env + history.jsonl + data/(*.db 一
|
||||
rm -f "$dest/data/$rel-wal" "$dest/data/$rel-shm" # .backup 已并入 WAL 内容
|
||||
else
|
||||
rm -f "$dest/data/$rel.bk" # 失败尝试的半成品不留在快照里
|
||||
if head -c 15 "$ENV_DATA/$rel" | grep -qa '^SQLite format 3$'; then
|
||||
if grep -qa '^SQLite format 3$' < <(head -c 15 "$ENV_DATA/$rel"); then
|
||||
c_err "[$ENV_NAME] SQLite 一致性备份/自检失败:$rel"
|
||||
return 1
|
||||
fi
|
||||
@@ -5446,7 +5522,7 @@ svc_pid_state() { # NAME → "alive <pid>" | "dead"
|
||||
[ -f "$d/svc.pid" ] || { printf dead; return 0; }
|
||||
pid="$(cat "$d/svc.pid" 2>/dev/null || true)"
|
||||
[ -n "$pid" ] || { printf dead; return 0; }
|
||||
mark="$(sed -n 's/^pidcheck=//p' "$d/svc.env" 2>/dev/null | head -1)"
|
||||
mark="$(sed -n '/^pidcheck=/{s///p;q;}' "$d/svc.env" 2>/dev/null)"
|
||||
[ -z "$mark" ] && mark="agent-system"
|
||||
if pid_alive_mark "$pid" "$mark"; then printf 'alive %s' "$pid"; else printf dead; fi
|
||||
return 0
|
||||
@@ -5479,7 +5555,7 @@ db_ref_of() {
|
||||
cf="$(env_kv_of "$name" conffile)"
|
||||
[ -z "$cf" ] && cf="$(printf '%s' "$(env_kv_of "$name" exec)" | sed -n 's/.*--config \([^ ]*\).*/\1/p')"
|
||||
if [ -n "$cf" ] && [ -f "$cf" ]; then
|
||||
dsn="$(grep -oE '"dsn"[[:space:]]*:[[:space:]]*"[^"]*"' "$cf" 2>/dev/null | head -1 | sed -E 's/.*"([^"]*)"$/\1/')"
|
||||
dsn="$(sed -n '/"dsn"[[:space:]]*:[[:space:]]*"[^"]*"/{s//\n&/;s/.*\n"dsn"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p;q;}' "$cf" 2>/dev/null)"
|
||||
fi
|
||||
fi
|
||||
[ -n "$dsn" ] || return 0
|
||||
@@ -5503,10 +5579,10 @@ global_list() {
|
||||
while IFS= read -r name; do names+=("$name"); done < <(registered_names)
|
||||
for name in ${names[@]+"${names[@]}"}; do
|
||||
d="$RUNTIME/$name"
|
||||
kind="$(sed -n 's/^kind=//p' "$d/svc.env" | head -1)"; [ -n "$kind" ] || kind=agent
|
||||
addr="$(sed -n 's/^addr=//p' "$d/svc.env" | head -1)"
|
||||
src="$(sed -n 's/^source=//p' "$d/svc.env" | head -1)"
|
||||
tags="$(sed -n 's/^tags=//p' "$d/svc.env" | head -1)"
|
||||
kind="$(sed -n '/^kind=/{s///p;q;}' "$d/svc.env")"; [ -n "$kind" ] || kind=agent
|
||||
addr="$(sed -n '/^addr=/{s///p;q;}' "$d/svc.env")"
|
||||
src="$(sed -n '/^source=/{s///p;q;}' "$d/svc.env")"
|
||||
tags="$(sed -n '/^tags=/{s///p;q;}' "$d/svc.env")"
|
||||
if [ -n "${LIST_TAG:-}" ]; then
|
||||
case ",$tags," in *",${LIST_TAG},"*) ;; *) continue ;; esac
|
||||
fi
|
||||
@@ -5553,6 +5629,8 @@ env_status_detail() {
|
||||
cfile="$(svc_get conffile)"
|
||||
[ -z "$cfile" ] && cfile="$(printf '%s' "$(svc_get exec)" | sed -n 's/.*--config \([^ ]*\).*/\1/p')"
|
||||
printf ' 配置文件 %s\n' "${cfile:-无(命令行参数形态;<name> config <file> 绑定)}"
|
||||
local saf; saf="$(svc_get superadmin_envfile)"
|
||||
[ -n "$saf" ] && printf ' 超管凭据文件 %s(仅登记路径)\n' "$saf"
|
||||
printf ' 监听守卫 insecureListen=%s allowedHosts=%s\n' \
|
||||
"$(env_var "$ENV_NAME" INSECURE_LISTEN)" \
|
||||
"$(env_var "$ENV_NAME" ALLOWED_HOSTS)"
|
||||
@@ -5673,7 +5751,7 @@ info_file_mode() {
|
||||
# info 遍历多服务时使用;无则空)。
|
||||
env_kv_of() {
|
||||
[ -f "$RUNTIME/$1/svc.env" ] || return 0
|
||||
sed -n "s/^$2=//p" "$RUNTIME/$1/svc.env" | head -1
|
||||
sed -n "/^$2=/{s///p;q;}" "$RUNTIME/$1/svc.env"
|
||||
}
|
||||
|
||||
# info_snapshot_lines FILE:逐行打印 KEY=VALUE 内容快照(按 CLI_REVEAL 决定掩码)。
|
||||
@@ -5936,7 +6014,7 @@ cmd_update() {
|
||||
# rdm@ 走独立 RDM 源仓。
|
||||
local latest_sha="" latest_rel="" latest_rdm="" need_rel=0 need_dev=0 need_rdm=0 e_src
|
||||
for e in ${targets[@]+"${targets[@]}"}; do
|
||||
e_src="$(sed -n 's/^source=//p' "$RUNTIME/$e/svc.env" | head -1)"
|
||||
e_src="$(sed -n '/^source=/{s///p;q;}' "$RUNTIME/$e/svc.env")"
|
||||
case "$e_src" in release@*) need_rel=1 ;; local@*|dev@*) need_dev=1 ;; rdm@*) need_rdm=1 ;; esac
|
||||
done
|
||||
local fetch_note="" src_args=""
|
||||
@@ -5974,7 +6052,7 @@ cmd_update() {
|
||||
for e in ${targets[@]+"${targets[@]}"}; do
|
||||
local kind src cur st_running
|
||||
kind="$(svc_kind "$e")"
|
||||
src="$(sed -n 's/^source=//p' "$RUNTIME/$e/svc.env" | head -1)"
|
||||
src="$(sed -n '/^source=/{s///p;q;}' "$RUNTIME/$e/svc.env")"
|
||||
to=""
|
||||
latest_disp=""
|
||||
case "$kind" in
|
||||
@@ -6083,7 +6161,7 @@ cmd_update() {
|
||||
# 每个服务独立子进程、清空环境,避免批内 AGENT_SYSTEM_* 串实例。
|
||||
# 原样带上 plugins=,避免在终端上再次询问,也避免 ${src##*@} 把插件提交当成基座。
|
||||
local plug plug_args=()
|
||||
plug="$(sed -n 's/^plugins=//p' "$RUNTIME/$name/svc.env" | head -1)"
|
||||
plug="$(sed -n '/^plugins=/{s///p;q;}' "$RUNTIME/$name/svc.env")"
|
||||
if [ -n "$plug" ]; then
|
||||
plug_args=(--with "$plug")
|
||||
else
|
||||
@@ -6429,6 +6507,12 @@ main() {
|
||||
exit 2
|
||||
fi
|
||||
env_config "$ENV_NAME"
|
||||
case "$action" in
|
||||
start|restart|deploy)
|
||||
if [ "$SVC_KIND" = agent ]; then
|
||||
svc_superadmin_envfile_check || exit 1
|
||||
fi ;;
|
||||
esac
|
||||
# 生命周期动作持 .start.lock(wait 模式;进程退出自动释放)——deploy 全程含
|
||||
# 构建窗口,防 recover/并发 start 在停机-换产物间隙抢跑。只读动作不加锁。
|
||||
case "$action" in
|
||||
@@ -6507,7 +6591,13 @@ finally:
|
||||
PY
|
||||
svc_db_marker_set pending || exit 1
|
||||
svc_db_identity_ensure || { c_err "[$ENV_NAME] adopt ensure 失败"; exit 1; }
|
||||
c_ok "[$ENV_NAME] adopt 完成,db_marker=established" ;;
|
||||
local adopt_marker; adopt_marker="$(svc_db_marker)"
|
||||
if [ "$adopt_marker" = established ]; then
|
||||
c_ok "[$ENV_NAME] adopt 完成,db_marker=established"
|
||||
else
|
||||
c_warn "[$ENV_NAME] adopt 未建立 DB 身份:db_marker=$adopt_marker(当前已装制品不具备 identity 能力,无法建立身份)"
|
||||
c_warn " 部署具备 identity ensure/verify 的制品后,由候选身份门禁补齐"
|
||||
fi ;;
|
||||
status) env_status_detail ;;
|
||||
logs) [ -f "$ENV_LOG" ] && tail -n "${CLI_LOGS_N:-50}" -f "$ENV_LOG" || c_warn "[$ENV_NAME] 暂无日志" ;;
|
||||
esac
|
||||
|
||||
+15
-14
@@ -20,13 +20,13 @@ fetch_target() {
|
||||
}
|
||||
origin_transport_unavailable() { # $1=失败的远端命令 stderr → 仅明确的传输失败(DNS/连接超时/拒绝/重置/无路由,或本地缺失仓库路径)返回 0;认证/授权/权限与未知错误返回 1
|
||||
local err=$1 url path
|
||||
if printf '%s\n' "$err" | grep -qiE 'authentication failed|permission denied|unauthori[sz]ed|forbidden|HTTP[^0-9]*40[13]|\b40[13]\b|could not read (username|password)|terminal prompts disabled|invalid credentials|access denied|host key verification failed'; then
|
||||
if grep -qiE 'authentication failed|permission denied|unauthori[sz]ed|forbidden|HTTP[^0-9]*40[13]|\b40[13]\b|could not read (username|password)|terminal prompts disabled|invalid credentials|access denied|host key verification failed' <<<"$err"; then
|
||||
return 1
|
||||
fi
|
||||
if printf '%s\n' "$err" | grep -qiE 'could not resolve (host|hostname|proxy)|name or service not known|temporary failure in name resolution|nodename nor servname|connection timed out|operation timed out|timed out after|connection refused|connection reset|no route to host|network is unreachable|couldn.t connect to server'; then
|
||||
if grep -qiE 'could not resolve (host|hostname|proxy)|name or service not known|temporary failure in name resolution|nodename nor servname|connection timed out|operation timed out|timed out after|connection refused|connection reset|no route to host|network is unreachable|couldn.t connect to server' <<<"$err"; then
|
||||
return 0
|
||||
fi
|
||||
printf '%s\n' "$err" | grep -qF 'does not appear to be a git repository' || return 1
|
||||
grep -qF 'does not appear to be a git repository' <<<"$err" || return 1
|
||||
url=$(git remote get-url origin 2>/dev/null) || return 1
|
||||
case "$url" in
|
||||
file://*) path=${url#file://} ;;
|
||||
@@ -211,12 +211,12 @@ cmd_precheck() {
|
||||
cmd_start() {
|
||||
need_repo
|
||||
local name="${1:-}"; [ -n "$name" ] || die "usage: task.sh start <feature|fix|hotfix|release>/<slug>"
|
||||
echo "$name" | grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' || die "branch name must match (feature|fix|hotfix|release)/<slug>"
|
||||
grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' <<<"$name" || die "branch name must match (feature|fix|hotfix|release)/<slug>"
|
||||
local kind="${TASK_BRANCH_KIND:-standalone}" initiative="${TASK_INITIATIVE:-}"
|
||||
case "$kind" in standalone|initiative|child) ;; *) die "TASK_BRANCH_KIND must be standalone, initiative or child" ;; esac
|
||||
if [ "$kind" = child ]; then
|
||||
[ -n "$initiative" ] || die "child task requires INITIATIVE=<parent initiative branch>"
|
||||
echo "$initiative" | grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' || die "invalid parent initiative branch"
|
||||
grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' <<<"$initiative" || die "invalid parent initiative branch"
|
||||
case "${name#*/}" in "${initiative#*/}"-?*) ;; *) die "child slug must start with parent initiative slug plus '-'" ;; esac
|
||||
fi
|
||||
local slug planned_child=0
|
||||
@@ -312,8 +312,8 @@ cmd_start() {
|
||||
esac
|
||||
if [ "$kind" != child ] && [ -n "${TASK_PARENT_BRANCH:-}${TASK_PARENT_PR:-}" ]; then
|
||||
[ -n "${TASK_PARENT_BRANCH:-}" ] && [ -n "${TASK_PARENT_PR:-}" ] || die "stacked task requires TASK_PARENT_BRANCH and TASK_PARENT_PR"
|
||||
echo "$TASK_PARENT_BRANCH" | grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' || die "invalid parent task branch"
|
||||
echo "$TASK_PARENT_PR" | grep -qE '^https?://[^[:space:]]+/pulls/[0-9]+$' || die "TASK_PARENT_PR must be a parent PR URL"
|
||||
grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' <<<"$TASK_PARENT_BRANCH" || die "invalid parent task branch"
|
||||
grep -qE '^https?://[^[:space:]]+/pulls/[0-9]+$' <<<"$TASK_PARENT_PR" || die "TASK_PARENT_PR must be a parent PR URL"
|
||||
case "${name#*/}" in "${TASK_PARENT_BRANCH#*/}"-?*) ;; *) die "stacked child slug must start with parent slug plus '-'" ;; esac
|
||||
parent_head=$(git rev-parse --verify "refs/heads/${TASK_PARENT_BRANCH}^{commit}") || die "parent branch missing locally"
|
||||
git merge-base --is-ancestor "$rmain" "$parent_head" || die "parent branch does not include current origin/${TARGET_BRANCH}; sync it first"
|
||||
@@ -640,13 +640,13 @@ cmd_sync() {
|
||||
# 默认闭合方式是 close --self:任务分支在合入前把本分支的卡改为"已完成(合入前闭合)"并归档,
|
||||
# 随本 PR 合入;上述合入后回填只用于遗留卡。
|
||||
|
||||
card_of_branch() { sed -n 's/^- 工作区:.*分支:\([A-Za-z0-9._/-]\+\).*/\1/p' "$1" | head -1; }
|
||||
card_status() { sed -n 's/^.*;状态:\([^;]*\).*/\1/p' "$1" | head -1; }
|
||||
card_of_branch() { sed -n '/^- 工作区:.*分支:\([A-Za-z0-9._/-]\+\).*/{s//\1/p;q;}' "$1"; }
|
||||
card_status() { sed -n '/^.*;状态:\([^;]*\).*/{s//\1/p;q;}' "$1"; }
|
||||
# 两级分支元数据行:'- 分支类型:<kind>;initiative_id:…;root_target:…;immediate_target:…;accepting_requirements:…'。
|
||||
# 旧卡没有该行:按 standalone、root/immediate 均为目标分支、不接收新需求处理。
|
||||
card_field() { sed -n "s/^- 分支类型:.*;$2:\([^;]*\).*/\1/p" "$1" 2>/dev/null | head -1; }
|
||||
card_r0() { sed -n 's/^- 权威仓:.*;R0(initial_base_sha)= \([0-9a-f]\+\)[[:space:]]*$/\1/p' "$1" 2>/dev/null | head -1; }
|
||||
card_branch_kind() { local v; v=$(sed -n 's/^- 分支类型:\([^;]*\).*/\1/p' "$1" 2>/dev/null | head -1); printf '%s\n' "${v:-standalone}"; }
|
||||
card_field() { sed -n "/^- 分支类型:.*;$2:\([^;]*\).*/{s//\1/p;q;}" "$1" 2>/dev/null; }
|
||||
card_r0() { sed -n '/^- 权威仓:.*;R0(initial_base_sha)= \([0-9a-f]\+\)[[:space:]]*$/{s//\1/p;q;}' "$1" 2>/dev/null; }
|
||||
card_branch_kind() { local v; v=$(sed -n '/^- 分支类型:\([^;]*\).*/{s//\1/p;q;}' "$1" 2>/dev/null); printf '%s\n' "${v:-standalone}"; }
|
||||
card_initiative_id() { card_field "$1" initiative_id; }
|
||||
card_root_target() { local v; v=$(card_field "$1" root_target); printf '%s\n' "${v:-${TARGET_BRANCH}}"; }
|
||||
card_immediate_target() { local v; v=$(card_field "$1" immediate_target); printf '%s\n' "${v:-${TARGET_BRANCH}}"; }
|
||||
@@ -1067,7 +1067,7 @@ EOF
|
||||
-d "{\"head\":\"${headbr}\",\"base\":\"${pr_base}\",\"title\":\"Close merged task cards (${headbr})\",\"body\":\"Auto-generated by task.sh close; see commit message for evidence.\"}" 2>&1) || resp=""
|
||||
pr_url=$(printf '%s' "$resp" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("html_url","") if isinstance(d,dict) else "")' 2>/dev/null || true)
|
||||
[ -n "$pr_url" ] && break
|
||||
echo "[close] 建 PR 第 ${attempt} 次未成功${resp:+($(printf '%s' "$resp" | head -c 120))},退避重试" >&2
|
||||
echo "[close] 建 PR 第 ${attempt} 次未成功${resp:+($(head -c 120 <<<"$resp"))},退避重试" >&2
|
||||
[ "$attempt" = 4 ] || sleep $((attempt * 3))
|
||||
done
|
||||
if [ -z "$pr_url" ]; then
|
||||
@@ -1131,7 +1131,8 @@ cmd_finish() {
|
||||
# registered-root, owner, lock, recursive dirt and ignored-file checks have passed.
|
||||
check_worktree finish
|
||||
local -a removal=(worktree remove)
|
||||
if git ls-files --stage | grep -q '^160000 '; then
|
||||
# Read the whole listing: an early-exit reader SIGPIPEs git on large indexes and pipefail drops --force.
|
||||
if git ls-files --stage | awk '$1 == "160000" { found = 1 } END { exit !found }'; then
|
||||
removal+=(--force)
|
||||
fi
|
||||
if ! git -C "$mainrepo" "${removal[@]}" "$root"; then
|
||||
|
||||
Reference in New Issue
Block a user