fix(release): close staging activation safety gaps

This commit is contained in:
lili 2026-07-22 23:38:40 -07:00
parent 65aecc56aa
commit d48cc60bbf
9 changed files with 1137 additions and 180 deletions

View File

@ -6,7 +6,16 @@ set -euo pipefail
RC_COMMIT="${RC_COMMIT:-}"
RC_HOST="${RC_HOST:-root@100.64.0.7}"
RC_ROOT="${RC_ROOT:-/root/game-staging/releases}"
GENERATION_ATTESTATION_ROOT="${GENERATION_ATTESTATION_ROOT:-/root/game-staging/attestations}"
BOOTSTRAP_ADMIN_TOKEN_FILE="${BOOTSTRAP_ADMIN_TOKEN_FILE:-}"
ACTIVATION_TIMEOUT_SEC="${ACTIVATION_TIMEOUT_SEC:-180}"
ACTIVATION_MODE="activate"
if [ "${1:-}" = "--verify-r1-attestation" ]; then
ACTIVATION_MODE="verify-r1-attestation"
elif [ -n "${1:-}" ]; then
printf '未知参数:%s\n' "$1" >&2
exit 2
fi
if [ -n "${GENERATION_GATE_EVIDENCE_REF:-}" ] && [ -n "${GENERATION_GATE_REF:-}" ] \
&& [ "$GENERATION_GATE_EVIDENCE_REF" != "$GENERATION_GATE_REF" ]; then
printf 'GENERATION_GATE_EVIDENCE_REF 与 GENERATION_GATE_REF 冲突,拒绝选择其一继续。\n' >&2
@ -51,13 +60,22 @@ if [ -n "$STORAGE_AUDIT_REF" ] \
printf 'STORAGE_AUDIT_REF 非法:只接受 sha256:<64hex>:evidence/storage-audit.tsv留空则远端实时生成。\n' >&2
exit 2
fi
if [ "$ACTIVATION_MODE" = activate ] && [[ ! "$BOOTSTRAP_ADMIN_TOKEN_FILE" =~ ^/[A-Za-z0-9._/-]+$ ]]; then
printf 'BOOTSTRAP_ADMIN_TOKEN_FILE 必填,且必须是 mini-desktop 上的安全绝对路径。\n' >&2
exit 2
fi
printf '激活 staging RChost=%s commit=%s\n' "$RC_HOST" "$RC_COMMIT"
printf '注意:该操作会执行数据库迁移并切换五个 staging 服务。\n'
if [ "$ACTIVATION_MODE" = activate ]; then
printf '注意:该操作会执行数据库迁移并切换五个 staging 服务。\n'
else
printf '仅校验独立 R1 attestation不访问数据库、不切换服务。\n'
fi
"$SSH_BIN" -o ProxyCommand=none "$RC_HOST" bash -s -- \
"$RC_COMMIT" "$RC_ROOT" "$ACTIVATION_TIMEOUT_SEC" "$STORAGE_AUDIT_REF" \
"$GENERATION_GATE_EVIDENCE_REF" <<'REMOTE_SCRIPT'
"$GENERATION_GATE_EVIDENCE_REF" "$GENERATION_ATTESTATION_ROOT" "$ACTIVATION_MODE" \
"$BOOTSTRAP_ADMIN_TOKEN_FILE" <<'REMOTE_SCRIPT'
set -Eeuo pipefail
commit="$1"
@ -65,6 +83,9 @@ root="$2"
timeout_sec="$3"
expected_storage_audit_ref="$4"
expected_generation_gate_ref="$5"
attestation_root="$6"
activation_mode="$7"
bootstrap_admin_token_file="$8"
release="$root/$commit"
active_pointer="$root/active"
stable_prefix="gda-staging"
@ -79,16 +100,36 @@ pointer_changed=0
previous_was_active=0
previous="none"
previous_valid=0
migration_started=0
database_restore_required=0
database_restore_rc=not-run
bootstrap_token_validated=0
mkdir -p "$evidence"
chmod 700 "$evidence"
umask 077
exec > >(tee -a "$activation_log") 2>&1
if [ "$activation_mode" = activate ]; then
mkdir -p "$evidence"
chmod 700 "$evidence"
exec > >(tee -a "$activation_log") 2>&1
else
evidence="$(mktemp -d)"
trap 'rm -rf "$evidence"' EXIT
fi
log_step() {
printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$1"
}
delete_bootstrap_token() {
[ "$bootstrap_token_validated" -eq 1 ] || return 0
if [ -f "$bootstrap_admin_token_file" ] && [ ! -L "$bootstrap_admin_token_file" ]; then
rm -f -- "$bootstrap_admin_token_file"
log_step "bootstrap admin token 文件已安全删除"
else
log_step "bootstrap admin token 文件状态异常,拒绝跟随或删除 symlink"
return 1
fi
}
unit_name() {
printf '%s-%s' "$1" "$2"
}
@ -187,7 +228,8 @@ set +a
export SPRING_PROFILES_ACTIVE=staging
export AIGC_WORKER_URL=http://127.0.0.1:${worker_port}/generate
exec java -jar ${quoted_target}/game-cloud/huijing-server/target/huijing-server.jar \\
--server.port=${backend_port} --spring.cloud.nacos.discovery.enabled=${discovery_enabled}
--server.port=${backend_port} --spring.cloud.nacos.discovery.enabled=${discovery_enabled} \\
--spring.flyway.enabled=false
EOF
cat >"$launch_dir/studio.sh" <<EOF
@ -257,6 +299,24 @@ start_stack() {
wait_url "$label admin" "http://127.0.0.1:$admin_port/"
}
start_candidate_probe() {
local target="$1" label="$2" prefix="$3" studio_port="$4" admin_port="$5"
local agent_port="$6" worker_port="$7" launch_dir
launch_dir="$(write_launchers "$target" "$label" 18080 "$studio_port" "$admin_port" \
"$agent_port" "$worker_port" false)"
# 当前应用没有可证明关闭全部 @Scheduled 的全局开关,候选 backend 不得连接共享数据面启动。
printf 'candidate_backend=SKIPPED_NO_ISOLATED_DATA_PLANE\n' >"$evidence/candidate-backend.status"
run_unit "$(unit_name "$prefix" cheap-agent)" "$launch_dir/cheap-agent.sh"
wait_port "$label cheap-agent" "$agent_port"
run_unit "$(unit_name "$prefix" cheap-worker)" "$launch_dir/cheap-worker.sh"
wait_json_health "$label cheap-worker" "http://127.0.0.1:$worker_port/health" ok true
run_unit "$(unit_name "$prefix" studio)" "$launch_dir/studio.sh"
wait_url "$label studio" "http://127.0.0.1:$studio_port/"
run_unit "$(unit_name "$prefix" admin)" "$launch_dir/admin.sh"
wait_url "$label admin" "http://127.0.0.1:$admin_port/"
}
valid_prepared_release() {
local target="$1" expected="${2:-}"
[ -d "$target/.git" ] || return 1
@ -271,29 +331,55 @@ valid_prepared_release() {
}
verify_generation_gate_evidence() {
local ref="$1" expected_commit="$2" hash_and_path expected_hash relative_path actual_hash object_type
local ref="$1" expected_commit="$2" hash_and_path expected_hash relative_path actual_hash
local source_path canonical_root canonical_source component walk mode
hash_and_path="${ref#sha256:}"
expected_hash="${hash_and_path%%:*}"
relative_path="${hash_and_path#*:}"
# wrapper 已做路径语法校验;远端仍独立确认它是候选 commit 中的普通 tracked 文件。
object_type="$(git -C "$release" cat-file -t "$expected_commit:$relative_path" 2>/dev/null || true)"
[ "$object_type" = blob ] || {
log_step "生成门证据不是候选 commit 内的 tracked blob path=$relative_path"
# attestation 是 R1 执行方在 commit 之外生成的内容寻址文件hash 在外部引用中,避免“证明文件属于被证明 commit”的自引用。
[ "${relative_path##*/}" = "$expected_hash.attestation" ] || {
log_step "生成门 attestation 文件名未绑定 SHA-256 path=$relative_path"
return 1
}
[ -f "$release/$relative_path" ] && [ ! -L "$release/$relative_path" ] || {
log_step "生成门证据不是 RC 内普通文件 path=$relative_path"
[ -d "$attestation_root" ] && [ ! -L "$attestation_root" ] || {
log_step "attestation 根目录缺失或为 symlink root=$attestation_root"
return 1
}
actual_hash="$(sha256sum "$release/$relative_path" | awk '{print $1}')"
canonical_root="$(realpath "$attestation_root")" || return 1
[ "$canonical_root" = "$attestation_root" ] || {
log_step "attestation 根目录必须使用 canonical 绝对路径 root=$attestation_root"
return 1
}
walk="$attestation_root"
IFS='/' read -r -a path_components <<<"$relative_path"
for component in "${path_components[@]}"; do
walk="$walk/$component"
[ ! -L "$walk" ] || {
log_step "attestation 路径不得包含 symlink path=$relative_path"
return 1
}
done
source_path="$attestation_root/$relative_path"
[ -f "$source_path" ] || {
log_step "attestation 不是普通文件 path=$relative_path"
return 1
}
canonical_source="$(realpath "$source_path")" || return 1
[ "$canonical_source" = "$source_path" ] || return 1
mode="$(stat -c '%a' "$source_path" 2>/dev/null || stat -f '%Lp' "$source_path")"
[ "$mode" = 600 ] || {
log_step "attestation 权限必须为 0600 actual=$mode path=$relative_path"
return 1
}
actual_hash="$(sha256sum "$source_path" | awk '{print $1}')"
[ "$actual_hash" = "$expected_hash" ] || {
log_step "生成门证据 SHA-256 不匹配 path=$relative_path"
return 1
}
# 证据由 R1 执行方生成,激活器只验证,不创建或改写任何“通过”结论。
python3 - "$release/$relative_path" "$expected_commit" <<'PY'
python3 - "$source_path" "$expected_commit" <<'PY'
import sys
path, expected_commit = sys.argv[1:]
@ -336,6 +422,78 @@ PY
log_step "生成 R1 门证据校验 PASS path=$relative_path"
}
if [ "$activation_mode" = verify-r1-attestation ]; then
verify_generation_gate_evidence "$expected_generation_gate_ref" "$commit"
printf 'GENERATION_ATTESTATION_PASS commit=%s ref=%s\n' "$commit" "$expected_generation_gate_ref"
exit 0
fi
exec 9>"$root/.activation.lock"
if ! flock -n 9; then
log_step "已有 staging activation 正在执行,拒绝并发部署"
exit 75
fi
flyway_history() {
docker exec game-staging-mysql sh -lc \
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -uroot -N -B ruoyi-vue-pro -e "SELECT installed_rank,COALESCE(version,\"\"),description,type,script,checksum,success FROM flyway_schema_history ORDER BY installed_rank"'
}
run_flyway_migrations() {
local runtime_dir="$evidence/flyway-runtime" source_file="$evidence/StagingFlywayMigration.java"
local class_dir="$evidence/flyway-classes" jar="$release/game-cloud/huijing-server/target/huijing-server.jar"
mkdir -p "$runtime_dir" "$class_dir"
java -Djarmode=tools -jar "$jar" extract --destination "$runtime_dir" >/dev/null
cat >"$source_file" <<'JAVA'
import org.flywaydb.core.Flyway;
/** 只执行 Flyway不启动 Spring、消费者、定时任务或 Web 服务。 */
public final class StagingFlywayMigration {
public static void main(String[] args) {
if (args.length != 2) {
throw new IllegalArgumentException("需要 migration 与 staging migration 两个目录");
}
String url = System.getenv("STAGING_FLYWAY_URL");
String user = System.getenv("STAGING_FLYWAY_USER");
String password = System.getenv("STAGING_FLYWAY_PASSWORD");
if (url == null || user == null || password == null) {
throw new IllegalStateException("缺少 Flyway 数据库凭据");
}
Flyway flyway = Flyway.configure()
.dataSource(url, user, password)
.locations("filesystem:" + args[0], "filesystem:" + args[1])
.failOnMissingLocations(true)
.load();
flyway.validate();
var result = flyway.migrate();
System.out.printf("FLYWAY_MIGRATION_PASS executed=%d target=%s%n",
result.migrationsExecuted, result.targetSchemaVersion);
}
}
JAVA
javac -cp "$runtime_dir/lib/*" -d "$class_dir" "$source_file"
STAGING_FLYWAY_URL='jdbc:mysql://127.0.0.1:13306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true' \
STAGING_FLYWAY_USER=root STAGING_FLYWAY_PASSWORD="$MYSQL_ROOT_PASSWORD" \
java -cp "$class_dir:$runtime_dir/lib/*" StagingFlywayMigration \
"$release/game-cloud/huijing-server/src/main/resources/db/migration" \
"$release/game-cloud/huijing-server/src/main/resources/db/staging" \
>"$evidence/flyway-migration.log" 2>&1
grep -q '^FLYWAY_MIGRATION_PASS ' "$evidence/flyway-migration.log"
}
restore_database_snapshot() {
local dump="$evidence/pre-migration-ruoyi-vue-pro.sql.gz"
[ -s "$dump" ] || return 1
stop_stack "$stable_prefix"
docker exec game-staging-mysql sh -lc \
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -uroot -e "DROP DATABASE IF EXISTS `ruoyi-vue-pro`"' || return 1
gzip -dc "$dump" | docker exec -i game-staging-mysql sh -lc \
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -uroot' || return 1
flyway_history >"$evidence/flyway-after-compensation.tsv" || return 1
cmp -s "$evidence/flyway-before.tsv" "$evidence/flyway-after-compensation.tsv" || return 1
printf 'DATABASE_COMPENSATION=PASS snapshot=%s\n' "$dump" >"$evidence/database-compensation.status"
}
restore_active_pointer() {
local target="$1" temp="$root/.active-rollback-$run_id"
rm -f "$temp"
@ -355,6 +513,17 @@ failure_handler() {
capture_journals "$candidate_prefix" candidate-failed
capture_journals "$stable_prefix" stable-failed
stop_stack "$candidate_prefix"
if [ "$database_restore_required" -eq 1 ]; then
log_step "迁移后失败,停止新栈并恢复迁移前数据库快照"
restore_database_snapshot
database_restore_rc=$?
printf 'database_restore_rc=%s\n' "$database_restore_rc" >"$evidence/database-restore-result.txt"
if [ "$database_restore_rc" -ne 0 ]; then
log_step "数据库补偿失败,禁止宣称服务回滚成功"
printf 'FIRST_RC_FULL_ROLLBACK=UNAVAILABLE database_restore=FAILED\n' >"$evidence/rollback-result.txt"
exit 71
fi
fi
if [ "$switch_started" -eq 1 ]; then
stop_stack "$stable_prefix"
if [ "$previous_was_active" -eq 1 ] && [ "$previous_valid" -eq 1 ]; then
@ -363,12 +532,15 @@ failure_handler() {
rollback_rc=$?
printf 'rollback_service_rc=%s\n' "$rollback_rc" >"$evidence/rollback-result.txt"
else
printf 'rollback_service_rc=not-required\n' >"$evidence/rollback-result.txt"
printf 'rollback_service_rc=not-available\nFIRST_RC_FULL_ROLLBACK=UNAVAILABLE\n' >"$evidence/rollback-result.txt"
fi
fi
if [ "$pointer_changed" -eq 1 ]; then
restore_active_pointer "$previous"
fi
if [ "$bootstrap_token_validated" -eq 1 ]; then
log_step "激活失败,保留 0600 bootstrap admin token 供补偿后重试或人工吊销"
fi
printf 'failed commit=%s at=%s evidence=%s\n' "$commit" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$evidence" \
>"$release/RC_STATUS"
chmod -R go-rwx "$evidence" "$release/RC_STATUS"
@ -391,15 +563,17 @@ case "$(stat -c '%a' "$env_file")" in
*) log_step "拒绝读取权限过宽的 $env_file"; false ;;
esac
# 只校验键存在和非空,绝不输出值。
(
set -a
source "$env_file"
set +a
: "${MYSQL_ROOT_PASSWORD:?}" "${REDIS_PASSWORD:?}" "${NACOS_PASSWORD:?}"
: "${AIGC_CALLBACK_SECRET:?}" "${NEWAPI_KEY:?}"
[ "${AIGC_EXECUTOR_ENABLED:-}" = "true" ]
)
# 只校验键存在和非空,绝不输出值;后续独立 Flyway 进程从当前受控环境读取数据库凭据。
set -a
source "$env_file"
set +a
: "${MYSQL_ROOT_PASSWORD:?}" "${REDIS_PASSWORD:?}" "${NACOS_PASSWORD:?}"
: "${AIGC_CALLBACK_SECRET:?}" "${NEWAPI_KEY:?}"
[ -n "${DOGFOOD_PASSWORD_SEED:-}" ] || {
log_step "缺 DOGFOOD_PASSWORD_SEED无法确定性预置内测账号"
false
}
[ "${AIGC_EXECUTOR_ENABLED:-}" = "true" ]
cd "$release"
sha256sum -c evidence/artifacts.sha256 >"$evidence/artifact-verification.txt"
@ -436,6 +610,22 @@ if [ "$previous_was_active" -eq 1 ] && [ "$previous_valid" -ne 1 ]; then
fi
printf 'previous=%s\nprevious_was_active=%s\n' "$previous" "$previous_was_active" >"$evidence/activation-context.txt"
log_step "启动不接共享数据面的候选探针backend 因缺全局禁调度开关明确跳过"
for port in 14173 14174 18300 19501; do
if port_listening "$port"; then
log_step "候选端口已占用 port=$port"
false
fi
done
start_candidate_probe "$release" candidate "$candidate_prefix" 14173 14174 18300 19501
capture_journals "$candidate_prefix" candidate-ready
log_step "候选非数据面探针全绿,停止旧栈后执行独立 Flyway 迁移"
switch_started=1
capture_journals "$stable_prefix" pre-switch-old
stop_stack "$stable_prefix"
# 旧栈停写后再生成补偿快照,避免候选探针期间的新写入在失败恢复时丢失。
log_step "迁移前数据库快照"
docker inspect game-staging-mysql >/dev/null
docker exec game-staging-mysql sh -lc \
@ -452,31 +642,15 @@ EXPECTED_AUDIT_FILE="$evidence/storage-audit.tsv" MYSQL_IMAGE="$mysql_image" \
grep -q '^RESTORE_VERIFY_PASS ' "$evidence/mysql-restore-verify.log"
log_step "数据库快照隔离恢复验证 PASS"
log_step "启动候选栈backend 启动时先执行 Flyway migrations"
for port in 18080 14173 14174 18300 19501; do
if port_listening "$port"; then
log_step "候选端口已占用 port=$port"
false
fi
done
start_stack "$release" candidate "$candidate_prefix" 18080 14173 14174 18300 19501 false
docker exec game-staging-mysql sh -lc \
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -uroot -N -B ruoyi-vue-pro -e "SELECT installed_rank,version,success FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 20"' \
>"$evidence/flyway-after-candidate.tsv"
flyway_history >"$evidence/flyway-before.tsv"
migration_started=1
database_restore_required=1
run_flyway_migrations
flyway_history >"$evidence/flyway-after-migration.tsv"
flyway_failed_count="$(docker exec game-staging-mysql sh -lc \
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -uroot -N -B ruoyi-vue-pro -e "SELECT COUNT(*) FROM flyway_schema_history WHERE success <> 1"')"
[ "$flyway_failed_count" = "0" ]
log_step "候选栈执行只读 smoke"
BASE=http://127.0.0.1:18080 STUDIO_BASE=http://127.0.0.1:14173 \
bash "$release/deploy/smoke-test.sh" >"$evidence/candidate-smoke.log" 2>&1
capture_journals "$candidate_prefix" candidate-ready
log_step "候选栈全绿,开始切换正式五端口"
switch_started=1
capture_journals "$stable_prefix" pre-switch-old
stop_stack "$stable_prefix"
log_step "独立 Flyway 迁移 PASS开始切换正式五端口"
for port in 48080 4173 4174 8300 9501; do
if port_listening "$port"; then
log_step "正式端口仍被非受管进程占用 port=$port"
@ -485,6 +659,114 @@ for port in 48080 4173 4174 8300 9501; do
done
start_stack "$release" stable "$stable_prefix" 48080 4173 4174 8300 9501 true
log_step "校验 bootstrap admin token 文件"
[ -f "$bootstrap_admin_token_file" ] && [ ! -L "$bootstrap_admin_token_file" ]
bootstrap_token_mode="$(stat -c '%a' "$bootstrap_admin_token_file" 2>/dev/null || stat -f '%Lp' "$bootstrap_admin_token_file")"
[ "$bootstrap_token_mode" = 600 ] || {
log_step "bootstrap admin token 文件权限必须为 0600 actual=$bootstrap_token_mode"
false
}
bootstrap_token_validated=1
# 默认 admin 尚有效时先完成 40 人预置;事务返回即表示 Nacos 已逐字恢复,之后才允许禁用 admin。
log_step "使用 bootstrap admin token 执行 40 人账号预置事务"
account_provision_evidence="$evidence/account-provision"
ADMIN_TOKEN_FILE="$bootstrap_admin_token_file" \
DOGFOOD_PROVISION_ACK='PROVISION_DOGFOOD_WITH_NACOS_TRANSACTION' \
BASE=http://127.0.0.1:48080 MYSQL_SSH_HOST='' \
DOGFOOD_ACCOUNTS_FILE="$evidence/dogfood-accounts.tsv" \
EVIDENCE_DIR="$account_provision_evidence" \
bash "$release/deploy/provision-dogfood-with-nacos-transaction.sh" \
>"$evidence/account-provision-transaction.log" 2>&1
grep -q '^pass restored=true ' "$account_provision_evidence/transaction.status"
grep -q 'PROVISION_PASS accounts=40 creators=10 role_mismatches=0 claimed_accounts=40 duplicate_claims=0' \
"$account_provision_evidence/provision.log" || {
log_step "40 人终验缺失;若额度池不足,请先运行受控 new-api import 补建 neice_051-054"
false
}
chmod 600 "$evidence/account-provision-transaction.log" "$evidence/dogfood-accounts.tsv"
log_step "40 人账号与额度终验 PASSNacos 已恢复"
log_step "轮换并验证狗粮审核账号"
reviewer_credentials="$evidence/reviewer-credentials.json"
reviewer_evidence="$evidence/reviewer-evidence.json"
reviewer_runtime_evidence="$evidence/reviewer-runtime-verification.json"
python3 "$release/deploy/manage-dogfood-reviewers.py" \
--ack ROTATE_DOGFOOD_REVIEWERS \
--admin-token-file "$bootstrap_admin_token_file" \
--credentials-out "$reviewer_credentials" \
--evidence-out "$reviewer_evidence" \
>"$evidence/reviewer-rotation.log" 2>&1
# 工具已在轮换时验证默认公开密码失败;这里再验证新账号可登录,且被禁用 admin 的 bootstrap token 已失效。
python3 - "$reviewer_credentials" "$reviewer_evidence" "$bootstrap_admin_token_file" \
"$reviewer_runtime_evidence" <<'PY'
import json,os,stat,sys
from urllib.error import HTTPError,URLError
from urllib.request import Request,urlopen
credentials_path,evidence_path,token_path,output_path=map(os.path.abspath,sys.argv[1:])
def read_private(path):
flags=os.O_RDONLY|getattr(os,"O_NOFOLLOW",0)
fd=os.open(path,flags)
with os.fdopen(fd,"r",encoding="utf-8") as handle:
mode=stat.S_IMODE(os.fstat(handle.fileno()).st_mode)
if mode != 0o600 or not stat.S_ISREG(os.fstat(handle.fileno()).st_mode):
raise RuntimeError(f"敏感文件必须是 0600 普通文件 path={path}")
return handle.read()
credentials=json.loads(read_private(credentials_path))
rotation=json.loads(read_private(evidence_path))
bootstrap_token=read_private(token_path).strip()
if credentials.get("status") != "COMPLETE" or rotation.get("status") != "PASS":
raise RuntimeError("审核账号轮换输出未收口")
accounts=credentials.get("accounts") or {}
rotation_accounts={item.get("username"):item for item in rotation.get("accounts") or []}
if rotation_accounts.get("admin",{}).get("defaultPasswordLoginCode") not in (1002000000,1002000001):
raise RuntimeError("默认公开 admin 密码没有确定拒绝证据")
def request_json(request):
try:
with urlopen(request,timeout=10) as response:
return response.status,json.loads(response.read().decode("utf-8"))
except HTTPError as error:
body=error.read().decode("utf-8")
return error.code,json.loads(body) if body else {}
except (URLError,TimeoutError,json.JSONDecodeError) as error:
raise RuntimeError("审核账号运行态验证请求失败") from error
verified=[]
for username in ("dogfood_reviewer","dogfood_readonly"):
password=accounts.get(username)
if not isinstance(password,str) or not password:
raise RuntimeError(f"审核账号凭据缺失 username={username}")
payload=json.dumps({"username":username,"password":password}).encode("utf-8")
status,body=request_json(Request("http://127.0.0.1:48080/admin-api/system/auth/login",
data=payload,method="POST",headers={"Content-Type":"application/json","tenant-id":"1"}))
if status != 200 or body.get("code") != 0 or not (body.get("data") or {}).get("accessToken"):
raise RuntimeError(f"审核账号无法登录 username={username}")
verified.append(username)
status,body=request_json(Request(
"http://127.0.0.1:48080/admin-api/system/auth/get-permission-info",
method="GET",headers={"Authorization":f"Bearer {bootstrap_token}","tenant-id":"1"}))
if status == 200 and body.get("code") == 0:
raise RuntimeError("bootstrap admin 旧 token 仍然有效")
flags=os.O_WRONLY|os.O_CREAT|os.O_EXCL|getattr(os,"O_NOFOLLOW",0)
fd=os.open(output_path,flags,0o600)
with os.fdopen(fd,"w",encoding="utf-8") as handle:
json.dump({"status":"PASS","loginVerified":verified,
"defaultAdminPasswordRejected":True,"bootstrapTokenInvalidated":True},
handle,ensure_ascii=False,indent=2)
handle.write("\n")
PY
chmod 600 "$reviewer_credentials" "$reviewer_evidence" "$reviewer_runtime_evidence" \
"$evidence/reviewer-rotation.log"
log_step "狗粮审核账号轮换与运行态验证 PASS"
BASE=http://127.0.0.1:48080 STUDIO_BASE=http://127.0.0.1:4173 \
bash "$release/deploy/smoke-test.sh" >"$evidence/stable-smoke.log" 2>&1
capture_journals "$stable_prefix" stable-ready
@ -499,6 +781,7 @@ printf 'active commit=%s at=%s evidence=%s\n' "$commit" "$(date -u +%Y-%m-%dT%H:
stop_stack "$candidate_prefix"
chmod -R go-rwx "$evidence" "$release/RC_STATUS"
delete_bootstrap_token
trap - ERR
log_step "RC_ACTIVATION_PASS commit=$commit active=$release"
log_step "ACTIVATION_EVIDENCE=$evidence"

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash
# 在 mini-desktop 的全新目录准备一个 commit-pinned staging RC。
# 本脚本只克隆、构建和留证,不切换 current、不停止或启动任何服务。
# 本脚本只克隆、构建和留证,不切换 active、不停止或启动任何服务。
set -euo pipefail
RC_COMMIT="${RC_COMMIT:-}"
@ -85,28 +85,90 @@ printf '%s\n' "$previous" >"$evidence/rollback-target.txt"
printf 'admin_stage_env_sha256=%s\n' "$(sha256sum game-admin/.env.stage | awk '{print $1}')"
} >"$evidence/config-snapshot.txt"
printf '[1/6] 构建 Java 后端(跳过全量后端测试\n'
printf '[1/7] 构建 Java 后端(构建阶段跳过全量测试,下一步单独执行安全关键集\n'
(cd game-cloud && mvn clean install -pl huijing-server -am -DskipTests)
printf '[2/6] 构建 game-studio staging\n'
printf '[2/7] 运行安全关键 Java 测试\n'
security_java_tests=(
DogfoodStartupValidatorTest
DogfoodSecurityStartupValidatorTest
DogfoodRbacSeedTest
TokenAuthenticationFilterTest
AuthenticationEntryPointImplTest
PassportServiceImplTest
ProjectServiceImplTest
RuntimePackageServiceImplTest
RuntimePackageApiImplDogfoodTest
AdminRuntimeControllerDogfoodTest
AppFeedControllerDogfoodTest
AdControllerDogfoodTest
TradeControllerDogfoodTest
AdminNewapiQuotaControllerTest
NewapiQuotaClaimConsumerTest
NewapiQuotaServiceImplTest
)
security_test_selector="$(IFS=,; printf '%s' "${security_java_tests[*]}")"
(
cd game-cloud
mvn test -pl huijing-server -am \
-Dtest="$security_test_selector" -Dsurefire.failIfNoSpecifiedTests=false
) >"$evidence/security-java-tests.log" 2>&1
# 不能只相信 Maven 退出码:逐个核对 Surefire XML防止选择器漂移后零测试仍签发 RC。
verify_security_java_reports() {
python3 - "$evidence/security-java-tests-summary.tsv" "${security_java_tests[@]}" <<'PY'
import pathlib,sys,xml.etree.ElementTree as ET
output=pathlib.Path(sys.argv[1])
expected=sys.argv[2:]
rows={}
for report in pathlib.Path("game-cloud").glob("**/target/surefire-reports/TEST-*.xml"):
root=ET.parse(report).getroot()
name=root.attrib.get("name","").rsplit(".",1)[-1]
if name not in expected:
continue
tests=int(root.attrib.get("tests","0"))
failures=int(root.attrib.get("failures","0"))
errors=int(root.attrib.get("errors","0"))
skipped=int(root.attrib.get("skipped","0"))
if name in rows:
raise SystemExit(f"安全测试报告重复: {name}")
rows[name]=(tests,failures,errors,skipped)
missing=[name for name in expected if name not in rows]
if missing:
raise SystemExit("安全测试报告缺失: "+",".join(missing))
for name,(tests,failures,errors,skipped) in rows.items():
if tests < 1 or failures or errors:
raise SystemExit(f"安全测试未通过: {name} tests={tests} failures={failures} errors={errors}")
with output.open("w",encoding="utf-8") as handle:
handle.write("test_class\ttests\tfailures\terrors\tskipped\n")
for name in expected:
handle.write(name+"\t"+"\t".join(map(str,rows[name]))+"\n")
PY
chmod 600 "$evidence/security-java-tests-summary.tsv"
}
verify_security_java_reports
printf '[3/7] 构建 game-studio staging\n'
(cd game-studio && npm ci && npm run build -- --mode staging)
printf '[3/6] 构建 game-admin stage\n'
printf '[4/7] 构建 game-admin stage\n'
corepack prepare pnpm@9.15.4 --activate
(cd game-admin && pnpm install --frozen-lockfile && pnpm build:stage)
printf '[4/6] 准备 commit 内独立 Python 虚拟环境\n'
printf '[5/7] 准备 commit 内独立 Python 虚拟环境\n'
UV_BIN="${UV_BIN:-/root/.local/bin/uv}"
[ -x "$UV_BIN" ] || { printf '缺少 uv%s\n' "$UV_BIN" >&2; exit 5; }
(cd cheap-worker && "$UV_BIN" venv .venv && "$UV_BIN" pip install --python .venv/bin/python -r requirements.txt)
(cd tier2/gen-worker && "$UV_BIN" venv .venv && "$UV_BIN" pip install --python .venv/bin/python -r requirements.txt)
printf '[5/6] 运行发布工具回归测试\n'
printf '[6/7] 运行发布工具回归测试\n'
release_tool_tests=(
deploy/tests/test-staging-rc.sh
deploy/tests/test-provision-dogfood-users.sh
deploy/tests/test-provision-dogfood-nacos-transaction.sh
deploy/tests/test-import-newapi-pool.sh
deploy/tests/test-dogfood-smoke.sh
)
(
export PATH="$release/cheap-worker/.venv/bin:$PATH"
@ -116,10 +178,14 @@ release_tool_tests=(
done
printf 'RUN game-runtime/tools/test_newapi_pool_provision.py\n'
python3 -m unittest game-runtime/tools/test_newapi_pool_provision.py
printf 'RELEASE_TOOL_TESTS_PASS shell=%s python=1\n' "${#release_tool_tests[@]}"
printf 'RUN deploy/tests/test-dogfood-reviewer-security.py\n'
python3 -m unittest deploy/tests/test-dogfood-reviewer-security.py
printf 'RUN deploy/dogfood-ops/tests/test_dogfood_ops.py\n'
python3 -m unittest deploy/dogfood-ops/tests/test_dogfood_ops.py
printf 'RELEASE_TOOL_TESTS_PASS shell=%s python=3\n' "${#release_tool_tests[@]}"
) >"$evidence/release-tool-tests.log" 2>&1
printf '[6/6] 记录制品与启动坐标\n'
printf '[7/7] 记录制品与启动坐标\n'
{
printf 'backend_jar=%s/game-cloud/huijing-server/target/huijing-server.jar\n' "$release"
printf 'studio_dist=%s/game-studio/dist\n' "$release"
@ -142,9 +208,13 @@ printf '[6/6] 记录制品与启动坐标\n'
printf 'release_tool_shell_tests=PASS\n'
printf 'release_tool_shell_test_count=%s\n' "${#release_tool_tests[@]}"
printf 'newapi_pool_python_tests=PASS\n'
printf 'dogfood_reviewer_python_tests=PASS\n'
printf 'dogfood_ops_python_tests=PASS\n'
printf 'backend_build=PASS\n'
printf 'backend_tests=NOT_RUN\n'
printf 'backend_tests_reason=maven_build_used_DskipTests\n'
printf 'backend_security_tests=PASS\n'
printf 'backend_security_test_count=%s\n' "${#security_java_tests[@]}"
printf 'security_java_tests_sha256=%s\n' "$(sha256sum "$evidence/security-java-tests.log" | awk '{print $1}')"
printf 'security_java_tests_summary_sha256=%s\n' "$(sha256sum "$evidence/security-java-tests-summary.tsv" | awk '{print $1}')"
printf 'studio_build=PASS\n'
printf 'admin_build=PASS\n'
} >"$evidence/verification-summary.txt"
@ -156,5 +226,5 @@ trap - ERR
printf 'RC_PREPARE_PASS release=%s\n' "$release"
printf 'rollback_target=%s\n' "$previous"
printf '下一步由统一部署窗口读取 evidence/artifact-paths.env探活通过后再原子切换 current。\n'
printf '下一步由统一部署窗口读取 evidence/artifact-paths.env探活通过后再原子切换 active。\n'
REMOTE_SCRIPT

View File

@ -6,6 +6,8 @@ set -euo pipefail
readonly EXPECTED_ACK='PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG'
readonly CODE_USERNAME_EXISTS='1002090004'
readonly CODE_REGISTER_DISABLED='1002090007'
readonly CODE_AD_DOGFOOD_DISABLED='1111002006'
readonly CODE_TRADE_DOGFOOD_DISABLED='1106008000'
BASE="${BASE:-http://100.64.0.7:48080}"
TENANT="${TENANT:-1}"
@ -18,6 +20,9 @@ MYSQL_BIN="${MYSQL_BIN:-mysql}"
MYSQL_SSH_HOST="${MYSQL_SSH_HOST-__AUTO__}"
MYSQL_ENV_FILE="${MYSQL_ENV_FILE:-/root/huijing-dev.env}"
VERIFY_TIMEOUT_SEC="${VERIFY_TIMEOUT_SEC:-60}"
ADMIN_TOKEN_FILE="${ADMIN_TOKEN_FILE:-}"
ADMIN_USERNAME="${ADMIN_USERNAME:-}"
ADMIN_PASSWORD="${ADMIN_PASSWORD:-}"
log() { printf '%s\n' "$*"; }
die() { printf 'PROVISION_FAIL %s\n' "$*" >&2; exit 1; }
@ -29,8 +34,14 @@ require_env() {
done
}
require_env PROVISION_ACK DOGFOOD_PASSWORD_SEED ADMIN_USERNAME ADMIN_PASSWORD
require_env PROVISION_ACK DOGFOOD_PASSWORD_SEED
[ "$PROVISION_ACK" = "$EXPECTED_ACK" ] || die "PROVISION_ACK 不匹配;确认临时配置后必须精确设为 $EXPECTED_ACK"
if [ -n "$ADMIN_TOKEN_FILE" ]; then
[ -z "$ADMIN_USERNAME" ] && [ -z "$ADMIN_PASSWORD" ] || \
die "ADMIN_TOKEN_FILE 不能与 ADMIN_USERNAME/ADMIN_PASSWORD 混用"
else
require_env ADMIN_USERNAME ADMIN_PASSWORD
fi
# 只允许调用 mini-desktop 本机或 Tailscale 地址,防止误向其它环境批量建号。
BASE_HOST="$(python3 - "$BASE" <<'PY'
@ -59,6 +70,26 @@ fi
command -v curl >/dev/null || die "缺少 curl"
command -v python3 >/dev/null || die "缺少 python3"
read_private_token() {
python3 - "$1" <<'PY'
import os,stat,sys
path=sys.argv[1]
flags=os.O_RDONLY|getattr(os,"O_NOFOLLOW",0)
try:
fd=os.open(path,flags)
except OSError as error:
raise SystemExit(f"无法安全打开 ADMIN_TOKEN_FILE{error}")
with os.fdopen(fd,"r",encoding="utf-8") as handle:
metadata=os.fstat(handle.fileno())
if not stat.S_ISREG(metadata.st_mode) or stat.S_IMODE(metadata.st_mode) != 0o600:
raise SystemExit("ADMIN_TOKEN_FILE 必须是 0600 普通文件")
token=handle.read().strip()
if not token or any(character.isspace() for character in token):
raise SystemExit("ADMIN_TOKEN_FILE 内容为空或包含空白字符")
print(token,end="")
PY
}
umask 077
BODY_FILE="$(mktemp)"
TMP_ACCOUNTS="$(mktemp)"
@ -226,12 +257,12 @@ read_missing_claim_user_ids() {
}
log "预置目标10 creator + 30 playerBASE host=$BASE_HOST"
log "运行前置:临时 dogfood=false、password-register/login=true、注册与登录 IP 时/日上限均为 60结束后恢复狗粮配置。"
log "运行前置:dogfood=true临时 password-register/provisioning=true注册与登录 IP 时/日上限均为 60结束后恢复配置。"
# dogfood=false 的只读行为校验;若仍在狗粮态,广告入口会被后端守卫拒绝。
# 临时开窗只能放行密码注册;广告和资金入口必须继续由 dogfood 后端守卫拒绝。
request '' GET /app-api/ad/slot/list-enabled
[ "$LAST_HTTP" = '200' ] && [ "$(json_value code)" = '0' ] \
|| die "临时 provisioning 配置未生效:要求 dogfood=falsehttp=$LAST_HTTP code=$(json_value code)"
[ "$LAST_HTTP" = '200' ] && [ "$(json_value code)" = "$CODE_AD_DOGFOOD_DISABLED" ] \
|| die "dogfood 广告守卫未保持关闭http=$LAST_HTTP code=$(json_value code)"
read_counts
MISSING_COUNT=$((40 - ACCOUNT_COUNT))
@ -242,18 +273,23 @@ UNCLAIMED_COUNT=$((40 - CLAIMED_ACCOUNT_COUNT))
[ "$UNCLAIMED_COUNT" -ge 0 ] || die "目标账号 claim 计数异常:$CLAIMED_ACCOUNT_COUNT"
REQUIRED_FREE=$((UNCLAIMED_COUNT + 12))
if [ "$FREE_COUNT" -lt "$REQUIRED_FREE" ]; then
die "额度池 FREE=${FREE_COUNT},不足以让剩余账号各 claim 一条并保留 12claimed_accounts=${CLAIMED_ACCOUNT_COUNT} required_free=${REQUIRED_FREE});请先受控补池,本脚本不会创建 new-api 资源"
die "额度池 FREE=${FREE_COUNT},不足以让剩余账号各 claim 一条并保留 12claimed_accounts=${CLAIMED_ACCOUNT_COUNT} required_free=${REQUIRED_FREE});请先运行受控 new-api import 补建 neice_051-054,本脚本不会创建 new-api 资源"
fi
# admin token 仅驻留内存,不输出、不写 accounts 文件。
ADMIN_BODY="$(json_body "$ADMIN_USERNAME" "$ADMIN_PASSWORD")"
request '' POST /admin-api/system/auth/login "$ADMIN_BODY"
[ "$LAST_HTTP" = '200' ] && [ "$(json_value code)" = '0' ] || die "admin 登录失败"
ADMIN_TOKEN="$(json_value data.accessToken)"
[ -n "$ADMIN_TOKEN" ] || die "admin 登录响应缺 accessToken"
if [ -n "$ADMIN_TOKEN_FILE" ]; then
ADMIN_TOKEN="$(read_private_token "$ADMIN_TOKEN_FILE")" || die "ADMIN_TOKEN_FILE 安全校验失败"
else
ADMIN_BODY="$(json_body "$ADMIN_USERNAME" "$ADMIN_PASSWORD")"
request '' POST /admin-api/system/auth/login "$ADMIN_BODY"
[ "$LAST_HTTP" = '200' ] && [ "$(json_value code)" = '0' ] || die "admin 登录失败"
ADMIN_TOKEN="$(json_value data.accessToken)"
[ -n "$ADMIN_TOKEN" ] || die "admin 登录响应缺 accessToken"
fi
printf 'username\trole\tuser_id\tpassword\n' >"$TMP_ACCOUNTS"
TARGET_USER_IDS=()
FUND_GUARD_VERIFIED=0
for index in "${!TARGET_NAMES[@]}"; do
username="${TARGET_NAMES[$index]}"
if [ "$index" -lt 10 ]; then role='creator'; else role='player'; fi
@ -285,6 +321,21 @@ for index in "${!TARGET_NAMES[@]}"; do
is_target_user_id "$user_id" && die "多个目标账号返回同一 userId=$user_id,拒绝继续"
TARGET_USER_IDS+=("$user_id")
# mock token 已关闭;只用首个真实登录账号的 token 验证一次资金后端守卫,且不落日志或证据。
if [ "$FUND_GUARD_VERIFIED" -eq 0 ]; then
PLAYER_TOKEN="$(json_value data.accessToken)"
[ -n "$PLAYER_TOKEN" ] || die "账号 $username 登录响应缺 accessToken无法验证资金守卫"
request "$PLAYER_TOKEN" GET /app-api/trade/charge/points-balance
[ "$LAST_HTTP" = '200' ] && [ "$(json_value code)" = "$CODE_TRADE_DOGFOOD_DISABLED" ] \
|| die "dogfood 充值守卫未保持关闭http=$LAST_HTTP code=$(json_value code)"
request "$PLAYER_TOKEN" GET '/app-api/trade/withdraw/page?pageNo=1&pageSize=1'
[ "$LAST_HTTP" = '200' ] && [ "$(json_value code)" = "$CODE_TRADE_DOGFOOD_DISABLED" ] \
|| die "dogfood 提现守卫未保持关闭http=$LAST_HTTP code=$(json_value code)"
unset PLAYER_TOKEN
FUND_GUARD_VERIFIED=1
log "已用真实玩家身份验证充值/提现狗粮守卫"
fi
if [ "$role" = 'creator' ]; then
request "$ADMIN_TOKEN" PUT /admin-api/passport/player/set-creator \
"{\"userId\":$user_id,\"creatorFlag\":1}"
@ -317,7 +368,7 @@ while :; do
&& [ "$DUPLICATE_CLAIM_COUNT" -eq 0 ] && [ "$FREE_COUNT" -ge 12 ]; then
break
fi
[ "$SECONDS" -lt "$deadline" ] || die "终验未收敛accounts=$ACCOUNT_COUNT creators=$CREATOR_COUNT role_mismatches=$ROLE_MISMATCH_COUNT claimed_accounts=$CLAIMED_ACCOUNT_COUNT duplicate_claims=$DUPLICATE_CLAIM_COUNT FREE=${FREE_COUNT}FREE 不足请受控补池,勿用本脚本创建 new-api 资源"
[ "$SECONDS" -lt "$deadline" ] || die "终验未收敛accounts=$ACCOUNT_COUNT creators=$CREATOR_COUNT role_mismatches=$ROLE_MISMATCH_COUNT claimed_accounts=$CLAIMED_ACCOUNT_COUNT duplicate_claims=$DUPLICATE_CLAIM_COUNT FREE=${FREE_COUNT}额度池不足请先运行受控 new-api import 补建 neice_051-054"
sleep 2
done

View File

@ -10,9 +10,19 @@ readonly CODE_USERNAME_OR_PASSWORD_ERROR='1002090005'
readonly CODE_PASSWORD_REGISTER_DISABLED='1002090007'
readonly CODE_DOGFOOD_SMS_DISABLED='1002090008'
readonly CODE_AD_DOGFOOD_DISABLED='1111002006'
readonly CODE_TRADE_DOGFOOD_DISABLED='1106008000'
MODE='transaction'
WATCHDOG_RECOVERY_DIR=''
if [ "${1:-}" = '--watchdog' ]; then
MODE='watchdog'
WATCHDOG_RECOVERY_DIR="${2:-}"
elif [ -n "${1:-}" ]; then
printf 'DOGFOOD_NACOS_TX_FAIL 未知参数:%s\n' "$1" >&2
exit 2
fi
DOGFOOD_PROVISION_ACK="${DOGFOOD_PROVISION_ACK:-}"
ADMIN_TOKEN_FILE="${ADMIN_TOKEN_FILE:-}"
BASE="${BASE:-http://100.64.0.7:48080}"
TENANT="${TENANT:-1}"
NACOS_BASE_URL="${NACOS_BASE_URL:-http://100.64.0.8:8848/nacos}"
@ -23,11 +33,14 @@ BACKEND_UNIT="${BACKEND_UNIT:-gda-staging-backend}"
PROVISION_BIN="${PROVISION_BIN:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/provision-dogfood-users.sh}"
CURL_BIN="${CURL_BIN:-curl}"
SYSTEMCTL_BIN="${SYSTEMCTL_BIN:-systemctl}"
SYSTEMD_RUN_BIN="${SYSTEMD_RUN_BIN:-systemd-run}"
FLOCK_BIN="${FLOCK_BIN:-flock}"
SLEEP_BIN="${SLEEP_BIN:-sleep}"
RESTART_TIMEOUT_SEC="${RESTART_TIMEOUT_SEC:-180}"
DOGFOOD_NEGATIVE_TOKEN="${DOGFOOD_NEGATIVE_TOKEN:-test1}"
PROVISION_TTL_SEC="${PROVISION_TTL_SEC:-900}"
RUN_ID="${RUN_ID:-$(date -u +%Y%m%dT%H%M%SZ)-$$}"
EVIDENCE_DIR="${EVIDENCE_DIR:-/root/game-staging/evidence/dogfood-provision-$RUN_ID}"
LOCK_FILE="${LOCK_FILE:-/root/game-staging/locks/dogfood-provision.lock}"
EVENT_LOG=''
log() {
@ -38,14 +51,31 @@ log() {
}
die() { printf 'DOGFOOD_NACOS_TX_FAIL %s\n' "$*" >&2; exit 1; }
[ "$DOGFOOD_PROVISION_ACK" = "$EXPECTED_ACK" ] || \
die "DOGFOOD_PROVISION_ACK 不匹配;必须精确确认 $EXPECTED_ACK"
[[ "$RESTART_TIMEOUT_SEC" =~ ^[0-9]+$ ]] && [ "$RESTART_TIMEOUT_SEC" -ge 30 ] || \
die 'RESTART_TIMEOUT_SEC 必须是至少 30 秒的整数'
[ -x "$PROVISION_BIN" ] || die "预置脚本不可执行:$PROVISION_BIN"
[[ "$PROVISION_TTL_SEC" =~ ^[0-9]+$ ]] && [ "$PROVISION_TTL_SEC" -ge 60 ] || \
die 'PROVISION_TTL_SEC 必须是至少 60 秒的整数'
[ "$MODE" = watchdog ] || [ "$DOGFOOD_PROVISION_ACK" = "$EXPECTED_ACK" ] || \
die "DOGFOOD_PROVISION_ACK 不匹配;必须精确确认 $EXPECTED_ACK"
[ "$MODE" = watchdog ] || [ -x "$PROVISION_BIN" ] || die "预置脚本不可执行:$PROVISION_BIN"
[ "$MODE" = watchdog ] || [ -n "$ADMIN_TOKEN_FILE" ] || die 'ADMIN_TOKEN_FILE 必填'
[ "$MODE" = watchdog ] || { [ -f "$ADMIN_TOKEN_FILE" ] && [ ! -L "$ADMIN_TOKEN_FILE" ]; } || \
die 'ADMIN_TOKEN_FILE 必须是非 symlink 普通文件'
if [ "$MODE" != watchdog ]; then
token_mode="$(stat -f '%Lp' "$ADMIN_TOKEN_FILE" 2>/dev/null || stat -c '%a' "$ADMIN_TOKEN_FILE")"
[ "$token_mode" = 600 ] || die "ADMIN_TOKEN_FILE 权限必须为 0600 actual=$token_mode"
fi
command -v python3 >/dev/null || die '缺少 python3'
python3 -c 'import yaml' >/dev/null 2>&1 || die '缺少 PyYAML无法安全合并既有 Nacos YAML'
mkdir -p "$(dirname "$LOCK_FILE")"
exec 9>"$LOCK_FILE"
if [ "$MODE" = watchdog ]; then
"$FLOCK_BIN" -w 30 9 || die 'watchdog 无法取得预置恢复锁'
else
"$FLOCK_BIN" -n 9 || die '已有 dogfood 账号预置事务运行,拒绝并发'
fi
if [ -z "$NACOS_PASSWORD" ]; then
[ -f "$NACOS_ENV_FILE" ] || die "缺少 Nacos 凭据来源:$NACOS_ENV_FILE"
set -a
@ -57,11 +87,16 @@ fi
[ -n "$NACOS_PASSWORD" ] || die 'Nacos 口令为空'
umask 077
[ ! -e "$EVIDENCE_DIR" ] || die "证据目录已存在,拒绝覆盖:$EVIDENCE_DIR"
mkdir -p "$EVIDENCE_DIR"
if [ "$MODE" = watchdog ]; then
[ -d "$WATCHDOG_RECOVERY_DIR" ] && [ ! -L "$WATCHDOG_RECOVERY_DIR" ] || die 'watchdog recovery 目录非法'
EVIDENCE_DIR="$(dirname "$WATCHDOG_RECOVERY_DIR")"
else
[ ! -e "$EVIDENCE_DIR" ] || die "证据目录已存在,拒绝覆盖:$EVIDENCE_DIR"
mkdir -p "$EVIDENCE_DIR"
fi
chmod 700 "$EVIDENCE_DIR"
EVENT_LOG="$EVIDENCE_DIR/transaction.log"
: >"$EVENT_LOG"
[ "$MODE" = watchdog ] || : >"$EVENT_LOG"
chmod 600 "$EVENT_LOG"
BODY_FILE="$(mktemp)"
NACOS_PASSWORD_FILE="$(mktemp)"
@ -72,8 +107,11 @@ ORIGINAL_CONFIG="$EVIDENCE_DIR/nacos-original.yaml"
TEMP_CONFIG="$EVIDENCE_DIR/nacos-temporary.yaml"
ORIGINAL_STATUS_FILE="$EVIDENCE_DIR/nacos-original.status"
SUMMARY_FILE="$EVIDENCE_DIR/transaction.status"
RECOVERY_DIR="$EVIDENCE_DIR/watchdog"
WATCHDOG_UNIT="dogfood-provision-watchdog-${RUN_ID//[^A-Za-z0-9_-]/-}"
restore_required=0
finalizing=0
watchdog_armed=0
NACOS_TOKEN=''
LAST_HTTP=''
LAST_BODY=''
@ -130,16 +168,19 @@ nacos_get() {
[ "$LAST_HTTP" = 200 ] || [ "$LAST_HTTP" = 404 ]
}
nacos_publish() {
local content_file="$1"
request POST "$NACOS_BASE_URL/v1/cs/configs" \
"dataId=$DATA_ID&group=$NACOS_GROUP&tenant=" "$NACOS_TOKEN" "$content_file"
[ "$LAST_HTTP" = 200 ] && [ "$LAST_BODY" = true ]
nacos_md5() {
python3 - "$1" <<'PY'
import hashlib,sys
with open(sys.argv[1],"rb") as handle:
print(hashlib.md5(handle.read()).hexdigest())
PY
}
nacos_delete() {
request DELETE "$NACOS_BASE_URL/v1/cs/configs" \
"dataId=$DATA_ID&group=$NACOS_GROUP&tenant=" "$NACOS_TOKEN"
nacos_publish() {
local content_file="$1" expected_md5="$2"
[[ "$expected_md5" =~ ^[0-9a-f]{32}$ ]] || return 1
request POST "$NACOS_BASE_URL/v1/cs/configs" \
"dataId=$DATA_ID&group=$NACOS_GROUP&tenant=&casMd5=$expected_md5" "$NACOS_TOKEN" "$content_file"
[ "$LAST_HTTP" = 200 ] && [ "$LAST_BODY" = true ]
}
@ -181,9 +222,10 @@ expect_code() {
}
verify_temporary_runtime() {
# 匿名广告位在 dogfood=false 时应正常返回;真实注册/登录由随后 provision 脚本继续实证。
request GET "$BASE/app-api/ad/slot/list-enabled"
[ "$LAST_HTTP" = 200 ] && [ "$(json_value code)" = 0 ]
# 此时还没有真实玩家 token只校验免登录边界资金守卫由预置脚本在首次真实登录后验证。
expect_code 'provisioning SMS still disabled' "$CODE_DOGFOOD_SMS_DISABLED" POST \
/app-api/passport/send-sms-code '{"mobile":"13800000000"}'
expect_code 'provisioning ad still disabled' "$CODE_AD_DOGFOOD_DISABLED" GET /app-api/ad/slot/list-enabled
}
verify_restored_runtime() {
@ -198,32 +240,78 @@ verify_restored_runtime() {
expect_code 'SMS disabled' "$CODE_DOGFOOD_SMS_DISABLED" POST \
/app-api/passport/send-sms-code '{"mobile":"13800000000"}'
expect_code 'ad disabled' "$CODE_AD_DOGFOOD_DISABLED" GET /app-api/ad/slot/list-enabled
expect_code 'charge disabled' "$CODE_TRADE_DOGFOOD_DISABLED" GET \
/app-api/trade/charge/points-balance '' "$DOGFOOD_NEGATIVE_TOKEN"
expect_code 'withdraw disabled' "$CODE_TRADE_DOGFOOD_DISABLED" GET \
'/app-api/trade/withdraw/page?pageNo=1&pageSize=1' '' "$DOGFOOD_NEGATIVE_TOKEN"
}
restore_original() {
local current="$EVIDENCE_DIR/nacos-before-restore.tmp" temporary_md5
log '开始恢复原 Nacos 配置'
nacos_login || return 1
original_status="$(<"$ORIGINAL_STATUS_FILE")"
if [ "$original_status" = 200 ]; then
nacos_publish "$ORIGINAL_CONFIG" || return 1
verify_nacos_exact "$ORIGINAL_CONFIG" || return 1
elif [ "$original_status" = 404 ]; then
nacos_delete || return 1
verify_file="$EVIDENCE_DIR/nacos-restore-verify.tmp"
nacos_get "$verify_file" || return 1
[ "$LAST_HTTP" = 404 ] || return 1
else
return 1
fi
nacos_get "$current" || return 1
[ "$LAST_HTTP" = 200 ] || return 1
cmp -s "$TEMP_CONFIG" "$current" || return 1
temporary_md5="$(nacos_md5 "$TEMP_CONFIG")"
nacos_publish "$ORIGINAL_CONFIG" "$temporary_md5" || return 1
verify_nacos_exact "$ORIGINAL_CONFIG" || return 1
restart_backend || return 1
verify_restored_runtime || return 1
log '原 Nacos 配置与狗粮安全边界恢复完成'
}
arm_watchdog() {
mkdir -p "$RECOVERY_DIR"
chmod 700 "$RECOVERY_DIR"
{
printf 'schema=dogfood-provision-watchdog/1\n'
printf 'run_id=%s\n' "$RUN_ID"
printf 'ttl_sec=%s\n' "$PROVISION_TTL_SEC"
printf 'temporary_md5=%s\n' "$(nacos_md5 "$TEMP_CONFIG")"
printf 'original_md5=%s\n' "$(nacos_md5 "$ORIGINAL_CONFIG")"
} >"$RECOVERY_DIR/metadata"
: >"$RECOVERY_DIR/armed"
chmod 600 "$RECOVERY_DIR/metadata" "$RECOVERY_DIR/armed"
if ! "$SYSTEMD_RUN_BIN" --quiet --collect --unit="$WATCHDOG_UNIT" \
--on-active="${PROVISION_TTL_SEC}s" --property=Type=oneshot \
--property=Restart=on-failure --property=RestartSec=15s \
/bin/bash "$(realpath "${BASH_SOURCE[0]}")" --watchdog "$RECOVERY_DIR"; then
rm -f "$RECOVERY_DIR/armed"
return 1
fi
watchdog_armed=1
log "独立恢复 watchdog 已注册 unit=$WATCHDOG_UNIT ttl=${PROVISION_TTL_SEC}s"
}
disarm_watchdog() {
[ "$watchdog_armed" -eq 1 ] || return 0
rm -f "$RECOVERY_DIR/armed"
"$SYSTEMCTL_BIN" stop "$WATCHDOG_UNIT.timer" "$WATCHDOG_UNIT.service" >/dev/null 2>&1 || true
printf 'disarmed_at=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >"$RECOVERY_DIR/disarmed"
chmod 600 "$RECOVERY_DIR/disarmed"
watchdog_armed=0
}
if [ "$MODE" = watchdog ]; then
EVENT_LOG="$EVIDENCE_DIR/watchdog.log"
touch "$EVENT_LOG"
chmod 600 "$EVENT_LOG"
[ -f "$RECOVERY_DIR/armed" ] || {
log 'watchdog 已解除,无需恢复'
cleanup_body
exit 0
}
log 'watchdog TTL 到期,开始独立恢复 Nacos 安全配置'
if restore_original; then
rm -f "$RECOVERY_DIR/armed"
printf 'WATCHDOG_RESTORE=PASS at=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >"$RECOVERY_DIR/watchdog.status"
chmod 600 "$RECOVERY_DIR/watchdog.status"
cleanup_body
exit 0
fi
printf 'WATCHDOG_RESTORE=FAILED at=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >"$RECOVERY_DIR/watchdog.status"
chmod 600 "$RECOVERY_DIR/watchdog.status"
cleanup_body
die "watchdog 恢复失败,保留 armed 标记等待人工处置:$RECOVERY_DIR"
fi
finalize() {
local main_rc=$? restore_rc=0
[ "$finalizing" -eq 0 ] || return
@ -241,6 +329,7 @@ finalize() {
printf 'DOGFOOD_NACOS_TX_RESTORE_FAIL evidence=%s\n' "$EVIDENCE_DIR" >&2
exit 70
fi
disarm_watchdog
if [ "$main_rc" -ne 0 ]; then
printf 'provision_failed rc=%s restored=true evidence=%s\n' "$main_rc" "$EVIDENCE_DIR" >"$SUMMARY_FILE"
chmod 600 "$SUMMARY_FILE"
@ -260,59 +349,55 @@ nacos_login || die 'Nacos 登录失败'
nacos_get "$ORIGINAL_CONFIG" || die "读取 $DATA_ID 失败 http=$LAST_HTTP"
printf '%s\n' "$LAST_HTTP" >"$ORIGINAL_STATUS_FILE"
chmod 600 "$ORIGINAL_STATUS_FILE"
restore_required=1
[ "$LAST_HTTP" = 200 ] || die 'Nacos dataId 不存在,无法执行内容 CAS拒绝创建临时配置'
# 在原 YAML 上只覆盖预置窗口所需键;原文始终单独留证并在 EXIT trap 逐字恢复。
ORIGINAL_STATUS="$LAST_HTTP" python3 - "$ORIGINAL_CONFIG" "$TEMP_CONFIG" <<'PY'
import os,sys,yaml
PROVISION_TTL_SEC="$PROVISION_TTL_SEC" python3 - "$ORIGINAL_CONFIG" "$TEMP_CONFIG" <<'PY'
import datetime,os,sys,yaml
source,target=sys.argv[1:]
if os.environ["ORIGINAL_STATUS"] == "404":
doc={}
else:
with open(source,encoding="utf-8") as f:
doc=yaml.safe_load(f) or {}
with open(source,encoding="utf-8") as f:
doc=yaml.safe_load(f) or {}
if not isinstance(doc,dict):
raise SystemExit("Nacos staging 配置顶层必须是 YAML object")
# 若历史配置使用过 dotted key先移除同义键避免 Spring 扁平化后与嵌套键冲突;原文仍会逐字恢复。
for key in (
"huijing.dogfood.enabled",
"huijing.dogfood.provisioning-enabled",
"huijing.dogfood.provisioning-expires-at",
"huijing.dogfood.provisioning-run-id",
"wanxiang.passport.password-register-enabled",
"wanxiang.passport.password-login-enabled",
"wanxiang.passport.register-ip-limit-per-hour",
"wanxiang.passport.register-ip-limit-per-day",
"wanxiang.passport.login-ip-limit-per-hour",
"wanxiang.passport.login-ip-limit-per-day",
):
doc.pop(key,None)
huijing=doc.setdefault("huijing",{})
if not isinstance(huijing,dict): raise SystemExit("huijing 必须是 object")
dogfood=huijing.setdefault("dogfood",{})
if not isinstance(dogfood,dict): raise SystemExit("huijing.dogfood 必须是 object")
dogfood["enabled"]=False
dogfood["enabled"]=True
dogfood["provisioning-enabled"]=True
dogfood["provisioning-run-id"]=os.environ["RUN_ID"]
expires_at=datetime.datetime.now(datetime.timezone.utc)+datetime.timedelta(seconds=int(os.environ["PROVISION_TTL_SEC"]))
dogfood["provisioning-expires-at"]=expires_at.isoformat().replace("+00:00","Z")
wanxiang=doc.setdefault("wanxiang",{})
if not isinstance(wanxiang,dict): raise SystemExit("wanxiang 必须是 object")
passport=wanxiang.setdefault("passport",{})
if not isinstance(passport,dict): raise SystemExit("wanxiang.passport 必须是 object")
passport.update({
"password-register-enabled": True,
"password-login-enabled": True,
"register-ip-limit-per-hour": 60,
"register-ip-limit-per-day": 60,
"login-ip-limit-per-hour": 60,
"login-ip-limit-per-day": max(60,int(passport.get("login-ip-limit-per-day",60))),
})
passport["password-register-enabled"]=True
with open(target,"w",encoding="utf-8") as f:
yaml.safe_dump(doc,f,allow_unicode=True,sort_keys=False)
PY
chmod 600 "$TEMP_CONFIG"
nacos_publish "$TEMP_CONFIG" || die "发布临时 Nacos 配置失败 http=$LAST_HTTP"
arm_watchdog || die '无法注册独立恢复 watchdog未修改 Nacos'
original_md5="$(nacos_md5 "$ORIGINAL_CONFIG")"
nacos_publish "$TEMP_CONFIG" "$original_md5" || die "发布临时 Nacos 配置 CAS 失败 http=$LAST_HTTP"
restore_required=1
verify_nacos_exact "$TEMP_CONFIG" || die '临时 Nacos 配置回读不一致'
restart_backend || die '临时配置发布后 backend 未恢复健康'
verify_temporary_runtime || die '临时 dogfood=false 未在后端可信边界生效'
verify_temporary_runtime || die '窄 provisioning 开关未生效或安全守卫被关闭'
log '临时配置已生效,执行既有账号预置脚本'
PROVISION_ACK='PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG' \
"$PROVISION_BIN" >"$EVIDENCE_DIR/provision.log" 2>&1
env -u ADMIN_USERNAME -u ADMIN_PASSWORD \
PROVISION_ACK='PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG' DOGFOOD_PROVISIONING_MODE=narrow \
ADMIN_TOKEN_FILE="$ADMIN_TOKEN_FILE" "$PROVISION_BIN" >"$EVIDENCE_DIR/provision.log" 2>&1
chmod 600 "$EVIDENCE_DIR/provision.log"
log '账号预置脚本完成,退出时恢复原配置'

View File

@ -15,6 +15,9 @@ elif [ -n "${1:-}" ]; then
exit 2
fi
SYSTEMCTL_BIN="${SYSTEMCTL_BIN:-systemctl}"
PROC_ROOT="${PROC_ROOT:-/proc}"
PASS=0
FAIL=0
FAILED_ITEMS=()
@ -203,6 +206,50 @@ if [ "$MODE" = "dogfood-e2e" ]; then
log "${R}DOGFOOD_E2E_ACK 无效;必须显式设置为 I_ACKNOWLEDGE_DOGFOOD_WRITES${N}" >&2
exit 2
fi
require_env EXPECTED_RC_COMMIT
if [[ ! "$EXPECTED_RC_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then
log "${R}EXPECTED_RC_COMMIT 必须是完整 40 位小写提交哈希${N}" >&2
exit 2
fi
# 写模式只允许在 mini-desktop 本机对固定 staging 端口执行,禁止 ACK 配合 BASE 重定向到其它环境。
if [ "$BASE" != "http://127.0.0.1:48080" ] || [ "$STUDIO_BASE" != "http://127.0.0.1:4173" ]; then
log "${R}写 smoke 仅允许 BASE=http://127.0.0.1:48080、STUDIO_BASE=http://127.0.0.1:4173${N}" >&2
exit 2
fi
if [ "$(hostname -s)" != "mini-desktop" ]; then
log "${R}写 smoke 只能在 mini-desktop 本机执行${N}" >&2
exit 2
fi
STAGING_ACTIVE_POINTER="${STAGING_ACTIVE_POINTER:-/root/game-staging/releases/active}"
if [ ! -L "$STAGING_ACTIVE_POINTER" ]; then
log "${R}缺少 staging active 指针:$STAGING_ACTIVE_POINTER${N}" >&2
exit 2
fi
ACTIVE_RELEASE="$(readlink -f "$STAGING_ACTIVE_POINTER" 2>/dev/null || true)"
if [ -z "$ACTIVE_RELEASE" ] || [ "${ACTIVE_RELEASE##*/}" != "$EXPECTED_RC_COMMIT" ]; then
log "${R}active 指针与 EXPECTED_RC_COMMIT 不一致${N}" >&2
exit 2
fi
if [ ! -f "$ACTIVE_RELEASE/RC_STATUS" ] || \
! grep -q "^active commit=$EXPECTED_RC_COMMIT " "$ACTIVE_RELEASE/RC_STATUS"; then
log "${R}active RC_STATUS 未证明目标 commit 已激活${N}" >&2
exit 2
fi
# active 指针只能证明部署意图;运行中 backend 的真实命令行也必须来自同一 RC。
if ! "$SYSTEMCTL_BIN" is-active --quiet gda-staging-backend; then
log "${R}staging backend systemd unit 未运行${N}" >&2
exit 2
fi
BACKEND_PID="$("$SYSTEMCTL_BIN" show --property=MainPID --value gda-staging-backend 2>/dev/null || true)"
if ! [[ "$BACKEND_PID" =~ ^[1-9][0-9]*$ ]] || [ ! -r "$PROC_ROOT/$BACKEND_PID/cmdline" ]; then
log "${R}无法读取 staging backend 运行进程身份${N}" >&2
exit 2
fi
EXPECTED_BACKEND_JAR="$ACTIVE_RELEASE/game-cloud/huijing-server/target/huijing-server.jar"
if ! tr '\0' '\n' <"$PROC_ROOT/$BACKEND_PID/cmdline" | grep -Fqx -- "$EXPECTED_BACKEND_JAR"; then
log "${R}运行中的 backend 不属于 active RC${N}" >&2
exit 2
fi
fi
head_line "公开只读 readiness"
@ -390,8 +437,11 @@ if ! envelope_ok || ! printf '%s' "$LAST_BODY" | python3 -c 'import json,sys; gi
fi
mark_pass "readonly 审核写权限被拒绝且状态未变"
PRE_APPROVE_VERSION="$(scan_main_feed)"
PRE_APPROVE_SCAN_RC=$?
if PRE_APPROVE_VERSION="$(scan_main_feed)"; then
PRE_APPROVE_SCAN_RC=0
else
PRE_APPROVE_SCAN_RC=$?
fi
if [ "$PRE_APPROVE_SCAN_RC" -eq 0 ]; then
e2e_fail "审核通过前 gameId=$GAME_ID 已进入主 feedversionId=${PRE_APPROVE_VERSION}"
elif [ "$PRE_APPROVE_SCAN_RC" -eq 1 ]; then
@ -415,8 +465,11 @@ done
[ "$project_status" = "4" ] || e2e_fail "审核通过后项目未进入 published(4)"
mark_pass "project published status=4"
FEED_VERSION_ID="$(scan_main_feed)"
POST_APPROVE_SCAN_RC=$?
if FEED_VERSION_ID="$(scan_main_feed)"; then
POST_APPROVE_SCAN_RC=0
else
POST_APPROVE_SCAN_RC=$?
fi
[ "$POST_APPROVE_SCAN_RC" -eq 0 ] || e2e_fail "审核通过后未能在主 feed 找到 gameId=${GAME_ID}scanRc=${POST_APPROVE_SCAN_RC}"
[ "$FEED_VERSION_ID" = "$VERSION_ID" ] || e2e_fail "feed versionId=$FEED_VERSION_ID 与生成版本 $VERSION_ID 不一致"
mark_pass "exact game appears in main feed"

View File

@ -20,10 +20,10 @@ assert_contains() {
assert_exit_code() {
local expected="$1" actual="$2" message="$3"
[ "$actual" -eq "$expected" ] || fail "$messageexpected=$expected actual=$actual"
[ "$actual" -eq "$expected" ] || fail "${message}expected=${expected} actual=${actual}"
}
# 假 curl 只实现 readiness 所需响应,并记录方法与 URL测试不访问任何真实服务。
# 假 curl 实现完整写链状态机,并记录方法与 URL测试不访问任何真实服务。
mkdir -p "$TMP_DIR/bin"
cat >"$TMP_DIR/bin/curl" <<'FAKE_CURL'
#!/usr/bin/env bash
@ -31,26 +31,77 @@ set -euo pipefail
method="GET"
output=""
url=""
auth=""
data=""
while [ "$#" -gt 0 ]; do
case "$1" in
-X) method="$2"; shift 2 ;;
-o) output="$2"; shift 2 ;;
-w|-H|--connect-timeout|--max-time|--data) shift 2 ;;
-H)
[[ "$2" == Authorization:* ]] && auth="${2#Authorization: Bearer }"
shift 2 ;;
-w|--connect-timeout|--max-time) shift 2 ;;
--data) data="$2"; shift 2 ;;
-sS) shift ;;
http://*|https://*) url="$1"; shift ;;
*) shift ;;
esac
done
printf '%s %s\n' "$method" "$url" >>"$REQUEST_LOG"
case "$url" in
*/actuator/health)
body='{"status":"UP"}' ;;
*/app-api/feed/zones)
body='{"code":0,"data":[{"id":1}]}' ;;
*/app-api/feed/stream*)
body='{"code":0,"data":{"list":[{"gameId":11,"versionId":22}],"hasMore":false}}' ;;
*/app-api/runtime/package/22*)
approved_file="$REQUEST_LOG.approved"
telemetry_file="$REQUEST_LOG.telemetry"
case "$method $url" in
"POST "*/app-api/passport/password-login)
body='{"code":0,"data":{"accessToken":"creator-token","userId":101,"creatorFlag":1}}' ;;
"POST "*/admin-api/system/auth/login)
if grep -q reviewer <<<"$data"; then token=reviewer-token; else token=readonly-token; fi
body="{\"code\":0,\"data\":{\"accessToken\":\"$token\"}}" ;;
"POST "*/app-api/passport/password-register)
body='{"code":1002090007,"data":null}' ;;
"POST "*/app-api/passport/send-sms-code)
body='{"code":1002090008,"data":null}' ;;
"GET "*/app-api/ad/slot/list-enabled)
body='{"code":1111002006,"data":null}' ;;
"GET "*/app-api/trade/charge/points-balance|"GET "*/app-api/trade/withdraw/page*)
body='{"code":1106008000,"data":null}' ;;
"POST "*/app-api/studio/create)
body='{"code":0,"data":{"id":31,"aigcTaskId":32,"gameId":11}}' ;;
"GET "*/app-api/studio/task/31)
body='{"code":0,"data":{"status":"2","aigcTaskId":32,"gameId":11,"versionId":22}}' ;;
"GET "*/app-api/runtime/package/22/manifest*)
body='{"entry":"index.html"}' ;;
"GET "*/app-api/runtime/package/22*)
body='{"code":0,"data":{"gameId":11,"versionId":22,"checksum":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}' ;;
"POST "*/app-api/project/11/publish)
body='{"code":0,"data":{"admitted":true,"gates":[]}}' ;;
"GET "*/admin-api/project/review/page*)
body='{"code":0,"data":{"list":[{"id":11}]}}' ;;
"POST "*/admin-api/project/review)
if [ "$auth" = reviewer-token ]; then : >"$approved_file"; body='{"code":0,"data":true}';
else body='{"code":1004030000,"data":null}'; fi ;;
"GET "*/app-api/project/11)
body='{"code":0,"data":{"status":"4"}}' ;;
"POST "*/app-api/runtime/session/start)
body='{"code":0,"data":{"sessionId":41}}' ;;
"POST "*/app-api/runtime/session/end)
body='{"code":0,"data":true}' ;;
"POST "*/app-api/telemetry/events/batch)
: >"$telemetry_file"; body='{"code":0,"data":{"accepted":2,"rejected":0}}' ;;
"GET "*/admin-api/telemetry/game-stat/11)
if [ -f "$telemetry_file" ]; then body='{"code":0,"data":{"playCount":1,"playEndCount":1}}';
else body='{"code":0,"data":{"playCount":0,"playEndCount":0}}'; fi ;;
"GET "*/actuator/health)
body='{"status":"UP"}' ;;
"GET "*/app-api/feed/zones)
body='{"code":0,"data":[{"id":1}]}' ;;
"GET "*/app-api/feed/stream*)
if [ -f "$approved_file" ]; then
body='{"code":0,"data":{"list":[{"gameId":11,"versionId":22}],"hasMore":false}}'
else
body='{"code":0,"data":{"list":[{"gameId":99,"versionId":98}],"hasMore":false}}'
fi ;;
"GET "*/app-api/runtime/package/98*)
body='{"code":0,"data":{"gameId":99,"versionId":98,"checksum":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}}' ;;
*)
body='{"code":404,"data":null}' ;;
esac
@ -62,6 +113,26 @@ chmod +x "$TMP_DIR/bin/curl"
export PATH="$TMP_DIR/bin:$PATH"
export REQUEST_LOG
cat >"$TMP_DIR/bin/hostname" <<'SH'
#!/usr/bin/env bash
printf 'mini-desktop\n'
SH
chmod +x "$TMP_DIR/bin/hostname"
cat >"$TMP_DIR/bin/systemctl" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
if [ "$*" = 'is-active --quiet gda-staging-backend' ]; then
exit 0
fi
if [ "$*" = 'show --property=MainPID --value gda-staging-backend' ]; then
printf '4242\n'
exit 0
fi
exit 2
SH
chmod +x "$TMP_DIR/bin/systemctl"
bash -n "$SMOKE"
# 构建配置只能包含内网地址和模式开关,不能烘焙登录 token。
@ -94,6 +165,17 @@ set -e
assert_exit_code 2 "$missing_ack_rc" '缺 ACK 时没有 fail-closed'
[ ! -s "$REQUEST_LOG" ] || fail '缺 ACK 时已发出网络请求'
# 写模式固定到 mini-desktop stagingACK 不能把 BASE 重定向到任意远端。
set +e
BASE=http://example.invalid DOGFOOD_CREATOR_USERNAME=creator DOGFOOD_CREATOR_PASSWORD=creator-pass \
DOGFOOD_REVIEWER_USERNAME=reviewer DOGFOOD_REVIEWER_PASSWORD=reviewer-pass \
DOGFOOD_READONLY_USERNAME=readonly DOGFOOD_READONLY_PASSWORD=readonly-pass \
DOGFOOD_E2E_ACK=I_ACKNOWLEDGE_DOGFOOD_WRITES EXPECTED_RC_COMMIT=0000000000000000000000000000000000000000 \
PATH="$TMP_DIR/bin:$PATH" "$SMOKE" --dogfood-e2e >"$TMP_DIR/wrong-target.out" 2>&1
wrong_target_rc=$?
set -e
assert_exit_code 2 "$wrong_target_rc" '写模式允许重定向到非 staging BASE'
# 默认 readiness 只能发送 GET且假服务完整返回时应通过。
PATH="$TMP_DIR/bin:$PATH" "$SMOKE" >"$TMP_DIR/readiness.out" 2>&1
assert_contains "$TMP_DIR/readiness.out" 'SMOKE_VERDICT=PASS'
@ -110,4 +192,51 @@ set -e
assert_exit_code 2 "$unknown_rc" '未知参数未拒绝'
[ ! -s "$REQUEST_LOG" ] || fail '未知参数触发了网络请求'
# 完整写模式回归:审核前 scan_main_feed 返回 1 是正确缺席bash -e 不得提前退出。
: >"$REQUEST_LOG"
rm -f "$REQUEST_LOG.approved" "$REQUEST_LOG.telemetry"
active_root="$TMP_DIR/releases"
active_release="$active_root/0000000000000000000000000000000000000000"
mkdir -p "$active_release"
ln -s "$active_release" "$active_root/active"
printf 'active commit=0000000000000000000000000000000000000000 at=test evidence=test\n' >"$active_release/RC_STATUS"
active_release="$(readlink -f "$active_root/active")"
proc_root="$TMP_DIR/proc"
mkdir -p "$proc_root/4242"
printf 'java\0-jar\0%s\0' "$TMP_DIR/releases/wrong/game-cloud/huijing-server/target/huijing-server.jar" \
>"$proc_root/4242/cmdline"
set +e
DOGFOOD_CREATOR_USERNAME=creator DOGFOOD_CREATOR_PASSWORD=creator-pass \
DOGFOOD_REVIEWER_USERNAME=reviewer DOGFOOD_REVIEWER_PASSWORD=reviewer-pass \
DOGFOOD_READONLY_USERNAME=readonly DOGFOOD_READONLY_PASSWORD=readonly-pass \
DOGFOOD_E2E_ACK=I_ACKNOWLEDGE_DOGFOOD_WRITES EXPECTED_RC_COMMIT=0000000000000000000000000000000000000000 \
STAGING_ACTIVE_POINTER="$active_root/active" SYSTEMCTL_BIN="$TMP_DIR/bin/systemctl" PROC_ROOT="$proc_root" \
BASE=http://127.0.0.1:48080 STUDIO_BASE=http://127.0.0.1:4173 \
PATH="$TMP_DIR/bin:$PATH" bash "$SMOKE" --dogfood-e2e >"$TMP_DIR/wrong-process.out" 2>&1
wrong_process_rc=$?
set -e
assert_exit_code 2 "$wrong_process_rc" '写 smoke 未拒绝与 active RC 不一致的 backend 进程'
mkdir -p "$active_release/game-cloud/huijing-server/target"
printf 'java\0-jar\0%s\0' "$active_release/game-cloud/huijing-server/target/huijing-server.jar" \
>"$proc_root/4242/cmdline"
set +e
DOGFOOD_CREATOR_USERNAME=creator DOGFOOD_CREATOR_PASSWORD=creator-pass \
DOGFOOD_REVIEWER_USERNAME=reviewer DOGFOOD_REVIEWER_PASSWORD=reviewer-pass \
DOGFOOD_READONLY_USERNAME=readonly DOGFOOD_READONLY_PASSWORD=readonly-pass \
DOGFOOD_E2E_ACK=I_ACKNOWLEDGE_DOGFOOD_WRITES EXPECTED_RC_COMMIT=0000000000000000000000000000000000000000 \
STAGING_ACTIVE_POINTER="$active_root/active" SYSTEMCTL_BIN="$TMP_DIR/bin/systemctl" PROC_ROOT="$proc_root" \
BASE=http://127.0.0.1:48080 STUDIO_BASE=http://127.0.0.1:4173 \
E2E_TIMEOUT_SEC=5 POLL_INTERVAL_SEC=0 EVIDENCE_OUT="$TMP_DIR/e2e.json" \
PATH="$TMP_DIR/bin:$PATH" bash -e "$SMOKE" --dogfood-e2e >"$TMP_DIR/e2e.out" 2>&1
e2e_rc=$?
set -e
if [ "$e2e_rc" -ne 0 ]; then
sed -n '1,200p' "$TMP_DIR/e2e.out" >&2
fail "完整写模式回归失败 rc=$e2e_rc"
fi
assert_contains "$TMP_DIR/e2e.out" 'pre-approve game absent from main feed'
assert_contains "$TMP_DIR/e2e.out" 'DOGFOOD_E2E_VERDICT=PASS'
assert_contains "$TMP_DIR/e2e.json" '"status": "passed"'
printf 'PASS: dogfood smoke fail-closed regression\n'

View File

@ -15,6 +15,7 @@ mode_of() {
cat >"$TMP_DIR/fake-curl" <<'PY'
#!/usr/bin/env python3
import json,os,sys
import hashlib
from pathlib import Path
import yaml
@ -61,6 +62,11 @@ if "/v1/cs/configs" in url:
respond(500,"false"); raise SystemExit
content_arg=next((x for x in data if x.startswith("content@")),None)
if not content_arg: respond(400,"false"); raise SystemExit
joined="&".join(data)
cas=next((part.split("=",1)[1] for part in joined.split("&") if part.startswith("casMd5=")),"")
current=hashlib.md5(config.read_bytes()).hexdigest() if config.exists() else ""
if not cas or cas != current:
respond(200,"false"); raise SystemExit
config.write_bytes(Path(content_arg[len("content@"):]).read_bytes())
respond(200,"true"); raise SystemExit
if method == "DELETE":
@ -82,7 +88,9 @@ elif url.endswith("/app-api/passport/password-login"):
elif url.endswith("/app-api/passport/send-sms-code"):
code=1002090008 if enabled else 0
elif "/app-api/trade/charge/points-balance" in url or "/app-api/trade/withdraw/page" in url:
code=1106008000 if enabled else 0
# mock token 已关闭Nacos 事务若仍用 test1 探测受保护端点,应立即因 401 失败。
respond(401,json.dumps({"code":401,"data":None},separators=(",",":")))
raise SystemExit
else:
code=404
respond(200,json.dumps({"code":code,"data":[]},separators=(",",":")))
@ -94,6 +102,18 @@ set -euo pipefail
printf '%s\n' "$*" >>"$MOCK_SYSTEMCTL_LOG"
SH
cat >"$TMP_DIR/fake-systemd-run" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$*" >>"$MOCK_SYSTEMD_RUN_LOG"
[ "${MOCK_SYSTEMD_RUN_FAIL:-0}" != 1 ] || exit 19
SH
cat >"$TMP_DIR/fake-flock" <<'SH'
#!/usr/bin/env bash
exit 0
SH
cat >"$TMP_DIR/fake-provision" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
@ -101,48 +121,75 @@ python3 - <<'PY'
import os,yaml
p=os.environ["MOCK_NACOS_CONFIG"]
d=yaml.safe_load(open(p,encoding="utf-8"))
assert d["huijing"]["dogfood"]["enabled"] is False
assert d["huijing"]["dogfood"]["enabled"] is True
assert d["huijing"]["dogfood"]["provisioning-enabled"] is True
assert d["huijing"]["dogfood"]["provisioning-expires-at"]
assert d["huijing"]["dogfood"]["provisioning-run-id"] == os.environ["RUN_ID"]
ppt=d["wanxiang"]["passport"]
assert ppt["password-register-enabled"] is True
assert ppt["password-login-enabled"] is True
assert min(ppt["register-ip-limit-per-hour"],ppt["register-ip-limit-per-day"],
ppt["login-ip-limit-per-hour"],ppt["login-ip-limit-per-day"]) >= 60
PY
[ "${PROVISION_ACK:-}" = 'PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG' ]
[ "${DOGFOOD_PROVISIONING_MODE:-}" = 'narrow' ]
[ -f "${ADMIN_TOKEN_FILE:-}" ] && [ ! -L "$ADMIN_TOKEN_FILE" ]
[ "$(stat -f '%Lp' "$ADMIN_TOKEN_FILE" 2>/dev/null || stat -c '%a' "$ADMIN_TOKEN_FILE")" = 600 ]
[ -z "${ADMIN_PASSWORD:-}" ]
printf 'MOCK_PROVISION_CALLED\n'
[ "${MOCK_PROVISION_FAIL:-0}" != 1 ] || exit 23
SH
chmod +x "$TMP_DIR/fake-curl" "$TMP_DIR/fake-systemctl" "$TMP_DIR/fake-provision"
chmod +x "$TMP_DIR/fake-curl" "$TMP_DIR/fake-systemctl" "$TMP_DIR/fake-systemd-run" \
"$TMP_DIR/fake-flock" "$TMP_DIR/fake-provision"
run_tx() {
local evidence="$1"
DOGFOOD_PROVISION_ACK='PROVISION_DOGFOOD_WITH_NACOS_TRANSACTION' \
ADMIN_TOKEN_FILE="$TMP_DIR/bootstrap-admin.token" \
NACOS_PASSWORD='test-secret-must-not-leak' \
CURL_BIN="$TMP_DIR/fake-curl" SYSTEMCTL_BIN="$TMP_DIR/fake-systemctl" \
SYSTEMD_RUN_BIN="$TMP_DIR/fake-systemd-run" FLOCK_BIN="$TMP_DIR/fake-flock" \
PROVISION_BIN="$TMP_DIR/fake-provision" MOCK_NACOS_CONFIG="$TMP_DIR/nacos.yaml" \
MOCK_PUBLISH_COUNT="$TMP_DIR/publish-count" MOCK_SYSTEMCTL_LOG="$TMP_DIR/systemctl.log" \
MOCK_SYSTEMD_RUN_LOG="$TMP_DIR/systemd-run.log" LOCK_FILE="$TMP_DIR/provision.lock" \
EVIDENCE_DIR="$evidence" RUN_ID="$(basename "$evidence")" RESTART_TIMEOUT_SEC=30 \
bash "$SCRIPT"
}
bash -n "$SCRIPT"
run_watchdog() {
local recovery="$1"
NACOS_PASSWORD='test-secret-must-not-leak' CURL_BIN="$TMP_DIR/fake-curl" \
SYSTEMCTL_BIN="$TMP_DIR/fake-systemctl" FLOCK_BIN="$TMP_DIR/fake-flock" \
MOCK_NACOS_CONFIG="$TMP_DIR/nacos.yaml" MOCK_PUBLISH_COUNT="$TMP_DIR/publish-count" \
MOCK_SYSTEMCTL_LOG="$TMP_DIR/systemctl.log" LOCK_FILE="$TMP_DIR/provision.lock" \
RESTART_TIMEOUT_SEC=30 bash "$SCRIPT" --watchdog "$recovery"
}
# 场景一:原 dataId=404成功后必须 DELETE 回 404重复运行仍幂等。
bash -n "$SCRIPT"
printf 'bootstrap-token-from-file\n' >"$TMP_DIR/bootstrap-admin.token"
chmod 600 "$TMP_DIR/bootstrap-admin.token"
rg -q 'flock' "$SCRIPT" || fail "缺单实例锁"
rg -q 'casMd5' "$SCRIPT" || fail "Nacos 发布/恢复缺内容 CAS"
rg -q 'provisioning-expires-at' "$SCRIPT" || fail "窄 provisioning 开关缺 TTL"
rg -q 'provisioning-run-id' "$SCRIPT" || fail "窄 provisioning 开关缺运行版本令牌"
rg -q -- '--watchdog' "$SCRIPT" || fail "缺 SIGKILL 独立 watchdog"
! rg -q 'dogfood.*enabled.*False' "$SCRIPT" || fail "事务仍会关闭 dogfood 总守卫"
rg -q 'password-register-enabled.*True' "$SCRIPT" || fail "事务未临时打开密码注册"
! rg -q 'provisioning charge still disabled\|provisioning withdraw still disabled' "$SCRIPT" || \
fail "Nacos 临时窗口仍使用无效 mock token 探测受保护资金端点"
# 场景一:原 dataId=404 时无法做内容 CAS必须在修改 Nacos 和调用 provision 前 fail-closed。
rm -f "$TMP_DIR/nacos.yaml" "$TMP_DIR/publish-count"
run_tx "$TMP_DIR/evidence-404" >"$TMP_DIR/run-404.log"
set +e
run_tx "$TMP_DIR/evidence-404" >"$TMP_DIR/run-404.log" 2>&1
rc=$?
set -e
[ "$rc" -ne 0 ] || fail "原 404 仍允许创建临时 Nacos 配置"
[ ! -e "$TMP_DIR/nacos.yaml" ] || fail "原 404 成功后未删除临时配置"
rg -q '^404$' "$TMP_DIR/evidence-404/nacos-original.status" || fail "未记录原 404"
rg -q '^pass restored=true ' "$TMP_DIR/evidence-404/transaction.status" || fail "404 场景未收口"
[ "$(mode_of "$TMP_DIR/evidence-404/nacos-temporary.yaml")" = 600 ] || fail "临时配置证据权限不是 600"
while IFS= read -r evidence_file; do
[ "$(mode_of "$evidence_file")" = 600 ] || fail "证据文件权限不是 600$evidence_file"
done < <(find "$TMP_DIR/evidence-404" -type f -print)
if rg -q 'test-secret-must-not-leak' "$TMP_DIR/evidence-404"; then fail "Nacos 密钥进入证据"; fi
rm -f "$TMP_DIR/publish-count"
run_tx "$TMP_DIR/evidence-404-repeat" >"$TMP_DIR/run-404-repeat.log"
[ ! -e "$TMP_DIR/nacos.yaml" ] || fail "重复运行后未恢复 404"
# 场景二:已有配置必须逐字恢复,临时结构化合并不能抹掉 custom 键。
cat >"$TMP_DIR/original.yaml" <<'YAML'
custom:
@ -161,20 +208,42 @@ run_tx "$TMP_DIR/evidence-existing" >"$TMP_DIR/run-existing.log"
cmp -s "$TMP_DIR/original.yaml" "$TMP_DIR/nacos.yaml" || fail "已有 Nacos 配置未逐字恢复"
rg -q '^200$' "$TMP_DIR/evidence-existing/nacos-original.status" || fail "未记录原 200"
rg -q 'retained: true' "$TMP_DIR/evidence-existing/nacos-temporary.yaml" || fail "临时合并丢失 custom 键"
rg -q '^pass restored=true ' "$TMP_DIR/evidence-existing/transaction.status" || fail "已有配置场景未收口"
[ "$(mode_of "$TMP_DIR/evidence-existing/nacos-temporary.yaml")" = 600 ] || fail "临时配置证据权限不是 600"
rg -q -- '--watchdog' "$TMP_DIR/systemd-run.log" || fail "未注册独立 watchdog"
rg -q 'Restart=on-failure' "$TMP_DIR/systemd-run.log" || fail "watchdog 抢锁失败后不会自动重试"
# 场景三provision 失败仍必须恢复原 404并保留原失败码。
rm -f "$TMP_DIR/nacos.yaml" "$TMP_DIR/publish-count"
# watchdog 注册失败必须发生在 Nacos 写入前,且不能留下假的 armed 状态。
cp "$TMP_DIR/original.yaml" "$TMP_DIR/nacos.yaml"
rm -f "$TMP_DIR/publish-count"
set +e
MOCK_SYSTEMD_RUN_FAIL=1 run_tx "$TMP_DIR/evidence-watchdog-arm-fail" >"$TMP_DIR/watchdog-arm-fail.log" 2>&1
rc=$?
set -e
[ "$rc" -ne 0 ] || fail "watchdog 注册失败仍继续预置"
cmp -s "$TMP_DIR/original.yaml" "$TMP_DIR/nacos.yaml" || fail "watchdog 注册失败前已修改 Nacos"
[ ! -e "$TMP_DIR/evidence-watchdog-arm-fail/watchdog/armed" ] || fail "watchdog 注册失败留下 armed 假状态"
# 重复运行必须仍以最新原文做 CAS并在退出时逐字恢复。
rm -f "$TMP_DIR/publish-count"
run_tx "$TMP_DIR/evidence-existing-repeat" >"$TMP_DIR/run-existing-repeat.log"
cmp -s "$TMP_DIR/original.yaml" "$TMP_DIR/nacos.yaml" || fail "重复运行后未逐字恢复"
# 场景三provision 失败仍必须恢复原配置,并保留原失败码。
cp "$TMP_DIR/original.yaml" "$TMP_DIR/nacos.yaml"
rm -f "$TMP_DIR/publish-count"
set +e
MOCK_PROVISION_FAIL=1 run_tx "$TMP_DIR/evidence-provision-fail" >"$TMP_DIR/run-provision-fail.log" 2>&1
rc=$?
set -e
[ "$rc" -eq 23 ] || fail "provision 失败未保留 rc=23actual=$rc"
[ ! -e "$TMP_DIR/nacos.yaml" ] || fail "provision 失败后未恢复原 404"
cmp -s "$TMP_DIR/original.yaml" "$TMP_DIR/nacos.yaml" || fail "provision 失败后未恢复原配置"
rg -q '^provision_failed rc=23 restored=true ' "$TMP_DIR/evidence-provision-fail/transaction.status" || \
fail "provision 失败证据状态错误"
# 场景四:恢复 DELETE 失败必须硬失败 70不能把 provision 成功当事务成功。
rm -f "$TMP_DIR/nacos.yaml" "$TMP_DIR/publish-count"
cp "$TMP_DIR/original.yaml" "$TMP_DIR/nacos.yaml"
rm -f "$TMP_DIR/publish-count"
set +e
MOCK_RESTORE_FAIL=1 run_tx "$TMP_DIR/evidence-restore-fail" >"$TMP_DIR/run-restore-fail.log" 2>&1
rc=$?
@ -184,6 +253,29 @@ rg -q '^restore_failed main_rc=0 ' "$TMP_DIR/evidence-restore-fail/transaction.s
fail "恢复失败证据状态错误"
rg -q 'DOGFOOD_NACOS_TX_RESTORE_FAIL' "$TMP_DIR/run-restore-fail.log" || fail "恢复失败未明确报警"
# 场景五:模拟父进程 SIGKILL 后遗留临时配置;独立 watchdog 必须凭 armed + CAS 恢复原文。
watchdog_recovery="$TMP_DIR/evidence-existing/watchdog"
cp "$TMP_DIR/evidence-existing/nacos-temporary.yaml" "$TMP_DIR/nacos.yaml"
: >"$watchdog_recovery/armed"
chmod 600 "$watchdog_recovery/armed"
rm -f "$TMP_DIR/publish-count"
run_watchdog "$watchdog_recovery" >"$TMP_DIR/watchdog.log"
cmp -s "$TMP_DIR/original.yaml" "$TMP_DIR/nacos.yaml" || fail "watchdog 未恢复原配置"
rg -q '^WATCHDOG_RESTORE=PASS ' "$watchdog_recovery/watchdog.status" || fail "watchdog 未留下恢复证据"
[ ! -e "$watchdog_recovery/armed" ] || fail "watchdog 成功后仍保留 armed"
# CAS 冲突时 watchdog 不能覆盖外部新配置,必须保留 armed 并硬失败等待人工处置。
printf 'external: changed\n' >"$TMP_DIR/nacos.yaml"
: >"$watchdog_recovery/armed"
chmod 600 "$watchdog_recovery/armed"
set +e
run_watchdog "$watchdog_recovery" >"$TMP_DIR/watchdog-cas-conflict.log" 2>&1
rc=$?
set -e
[ "$rc" -ne 0 ] || fail "watchdog CAS 冲突仍报告成功"
rg -q '^external: changed$' "$TMP_DIR/nacos.yaml" || fail "watchdog CAS 冲突覆盖了外部配置"
[ -e "$watchdog_recovery/armed" ] || fail "watchdog CAS 冲突后错误解除 armed"
restart_count="$(wc -l <"$TMP_DIR/systemctl.log" | tr -d ' ')"
[ "$restart_count" -ge 8 ] || fail "临时/恢复重启次数不足:$restart_count"

View File

@ -32,6 +32,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
state_file = os.environ["DOGFOOD_FAKE_STATE"]
port_file = os.environ["DOGFOOD_FAKE_PORT_FILE"]
request_log = os.environ["DOGFOOD_FAKE_REQUEST_LOG"]
def load_state():
@ -67,20 +68,39 @@ class Handler(BaseHTTPRequestHandler):
length = int(self.headers.get("Content-Length", "0"))
return json.loads(self.rfile.read(length) or b"{}")
def record(self):
with open(request_log, "a", encoding="utf-8") as handle:
handle.write(f"{self.command} {self.path}\n")
def do_GET(self):
self.record()
if self.path == "/app-api/ad/slot/list-enabled":
self.response({"code": 0, "data": []})
self.response({"code": 1111002006, "data": None})
return
if self.path == "/app-api/trade/charge/points-balance" or self.path.startswith(
"/app-api/trade/withdraw/page?"
):
authorization = self.headers.get("Authorization")
if authorization == "Bearer fake-player-token":
self.response({"code": 1106008000, "data": None})
else:
# mock token 已关闭test1 或无 token 都先被可信鉴权边界拒绝。
self.response({"code": 401, "data": None}, 401)
return
self.response({"code": 404}, 404)
def do_POST(self):
self.record()
body = self.json_body()
if self.path == "/admin-api/system/auth/login":
self.response({"code": 0, "data": {"accessToken": "fake-admin-token"}})
return
state = load_state()
if self.path.startswith("/admin-api/aigc/quota/claims/") and self.path.endswith("/reconcile"):
if self.headers.get("Authorization") != "Bearer fake-admin-token":
if self.headers.get("Authorization") not in (
"Bearer fake-admin-token",
"Bearer bootstrap-token-from-file",
):
self.response({"code": 401, "msg": "未授权"}, 401)
return
try:
@ -125,6 +145,7 @@ class Handler(BaseHTTPRequestHandler):
self.response({"code": 404}, 404)
def do_PUT(self):
self.record()
if self.path != "/admin-api/passport/player/set-creator":
self.response({"code": 404}, 404)
return
@ -222,9 +243,10 @@ chmod +x "$TMP_DIR/ssh"
DOGFOOD_FAKE_STATE="$TMP_DIR/state.json" \
DOGFOOD_FAKE_PORT_FILE="$TMP_DIR/port" \
DOGFOOD_FAKE_REQUEST_LOG="$TMP_DIR/requests.log" \
python3 "$TMP_DIR/fake-server.py" >"$TMP_DIR/server.log" 2>&1 &
SERVER_PID=$!
for _ in $(seq 1 50); do
for _ in $(seq 1 300); do
[ -s "$TMP_DIR/port" ] && break
sleep 0.1
done
@ -237,8 +259,7 @@ run_provision() {
local expected="${3:-pass}"
if ! PROVISION_ACK='PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG' \
DOGFOOD_PASSWORD_SEED='test-seed-must-not-appear' \
ADMIN_USERNAME='admin' \
ADMIN_PASSWORD='admin-password-must-not-appear' \
ADMIN_TOKEN_FILE="$TMP_DIR/bootstrap-admin.token" \
BASE="http://127.0.0.1:$PORT" \
MYSQL_BIN="$TMP_DIR/mysql" \
MYSQL_PASSWORD='fake-mysql-password' \
@ -257,8 +278,19 @@ run_provision() {
[ "$expected" = pass ] || fail "缺失 claim 的终验场景被错误放行:$DOGFOOD_FAKE_COUNTS_MODE"
}
printf 'bootstrap-token-from-file\n' >"$TMP_DIR/bootstrap-admin.token"
chmod 600 "$TMP_DIR/bootstrap-admin.token"
# 第一遍模拟 40 条注册消息全部异常:账号已创建但 claim 全缺,脚本必须逐个主动补偿。
run_provision "$TMP_DIR/first.log"
! rg -q '^POST /admin-api/system/auth/login$' "$TMP_DIR/requests.log" || \
fail "ADMIN_TOKEN_FILE 场景仍触发 admin 密码登录"
for protected_path in \
'/app-api/ad/slot/list-enabled' \
'/app-api/trade/charge/points-balance' \
'/app-api/trade/withdraw/page'; do
rg -q "^GET ${protected_path}" "$TMP_DIR/requests.log" || fail "未预检安全守卫:$protected_path"
done
python3 - "$TMP_DIR/state.json" <<'PY'
import json, sys
with open(sys.argv[1], encoding="utf-8") as handle:
@ -310,9 +342,42 @@ for log_file in "$TMP_DIR/first.log" "$TMP_DIR/second.log" "$TMP_DIR/recover-one
|| fail "缺少终验通过证据:$log_file"
! grep -q 'test-seed-must-not-appear' "$log_file" || fail "日志泄漏 seed"
! grep -q 'admin-password-must-not-appear' "$log_file" || fail "日志泄漏 admin 密码"
! grep -q 'fake-admin-token\|fake-player-token\|fake-mysql-password' "$log_file" || fail "日志泄漏 token 或数据库密码"
! grep -q 'bootstrap-token-from-file\|fake-admin-token\|fake-player-token\|fake-mysql-password' "$log_file" || \
fail "日志泄漏 token 或数据库密码"
done
# 手工场景仍可显式使用用户名密码;不能与 token 文件混用。
rm -f "$TMP_DIR/manual-requests.log"
mv "$TMP_DIR/requests.log" "$TMP_DIR/token-requests.log"
: >"$TMP_DIR/requests.log"
PROVISION_ACK='PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG' \
DOGFOOD_PASSWORD_SEED='test-seed-must-not-appear' ADMIN_USERNAME='admin' \
ADMIN_PASSWORD='admin-password-must-not-appear' BASE="http://127.0.0.1:$PORT" \
MYSQL_BIN="$TMP_DIR/mysql" MYSQL_PASSWORD='fake-mysql-password' MYSQL_SSH_HOST='' \
DOGFOOD_FAKE_STATE="$TMP_DIR/state.json" DOGFOOD_ACCOUNTS_FILE="$TMP_DIR/manual-accounts.tsv" \
VERIFY_TIMEOUT_SEC=2 PATH="$TMP_DIR:$PATH" "$SCRIPT" >"$TMP_DIR/manual.log" 2>&1
rg -q '^POST /admin-api/system/auth/login$' "$TMP_DIR/requests.log" || fail "手工账号密码模式未登录 admin"
# token 文件必须是 0600 普通文件,且两种认证来源不能同时提供。
chmod 644 "$TMP_DIR/bootstrap-admin.token"
if PROVISION_ACK='PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG' DOGFOOD_PASSWORD_SEED=x \
ADMIN_TOKEN_FILE="$TMP_DIR/bootstrap-admin.token" BASE="http://127.0.0.1:$PORT" \
"$SCRIPT" >"$TMP_DIR/wide-token.log" 2>&1; then
fail "权限过宽的 ADMIN_TOKEN_FILE 未被拒绝"
fi
chmod 600 "$TMP_DIR/bootstrap-admin.token"
ln -s "$TMP_DIR/bootstrap-admin.token" "$TMP_DIR/bootstrap-admin-link.token"
if PROVISION_ACK='PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG' DOGFOOD_PASSWORD_SEED=x \
ADMIN_TOKEN_FILE="$TMP_DIR/bootstrap-admin-link.token" BASE="http://127.0.0.1:$PORT" \
"$SCRIPT" >"$TMP_DIR/symlink-token.log" 2>&1; then
fail "symlink ADMIN_TOKEN_FILE 未被拒绝"
fi
if PROVISION_ACK='PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG' DOGFOOD_PASSWORD_SEED=x \
ADMIN_TOKEN_FILE="$TMP_DIR/bootstrap-admin.token" ADMIN_USERNAME=admin ADMIN_PASSWORD=secret \
BASE="http://127.0.0.1:$PORT" "$SCRIPT" >"$TMP_DIR/mixed-auth.log" 2>&1; then
fail "token 文件与用户名密码混用未被拒绝"
fi
# 数据库缺失查询若返回本次 40 人之外的编号,脚本必须在请求后端前拒绝,防止越权消耗额度。
DOGFOOD_FAKE_MISSING_MODE=rogue run_provision "$TMP_DIR/rogue-missing-id.log" '' fail
rg -q '不属于本次 40 个目标账号' "$TMP_DIR/rogue-missing-id.log" || fail "未拒绝越界补偿 userId"
@ -322,12 +387,12 @@ DOGFOOD_FAKE_COUNTS_MODE=duplicate run_provision "$TMP_DIR/duplicate-claims.log"
rg -q 'duplicate_claims=1' "$TMP_DIR/duplicate-claims.log" || fail "未报告重复 claim"
# 危险边界必须在任何网络请求前拒绝。
if PROVISION_ACK='wrong' DOGFOOD_PASSWORD_SEED=x ADMIN_USERNAME=x ADMIN_PASSWORD=x \
if PROVISION_ACK='wrong' DOGFOOD_PASSWORD_SEED=x ADMIN_TOKEN_FILE="$TMP_DIR/bootstrap-admin.token" \
BASE="http://127.0.0.1:$PORT" "$SCRIPT" >"$TMP_DIR/bad-ack.log" 2>&1; then
fail "错误 ACK 未被拒绝"
fi
if PROVISION_ACK='PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG' DOGFOOD_PASSWORD_SEED=x \
ADMIN_USERNAME=x ADMIN_PASSWORD=x BASE='http://localhost:48080' \
ADMIN_TOKEN_FILE="$TMP_DIR/bootstrap-admin.token" BASE='http://localhost:48080' \
"$SCRIPT" >"$TMP_DIR/bad-base.log" 2>&1; then
fail "非法 BASE host 未被拒绝"
fi

View File

@ -8,7 +8,7 @@ PREPARE="$ROOT_DIR/deploy/prepare-staging-rc.sh"
SMOKE="$ROOT_DIR/deploy/smoke-test.sh"
STORAGE_AUDIT="$ROOT_DIR/deploy/generate-staging-storage-audit.sh"
RESTORE_VERIFY="$ROOT_DIR/deploy/verify-mysql-dump-restore.sh"
GENERATION_GATE_REF="sha256:$(printf '0%.0s' $(seq 1 64)):evidence/generation-r1-gate.txt"
GENERATION_GATE_REF="sha256:$(printf '0%.0s' $(seq 1 64)):r1/$(printf '0%.0s' $(seq 1 64)).attestation"
TMP_DIR="$(mktemp -d)"
SERVER_PID=""
@ -41,6 +41,7 @@ chmod +x "$TMP_DIR/fake-ssh"
export RC_TEST_TMP="$TMP_DIR"
RC_COMMIT="0000000000000000000000000000000000000000" \
GENERATION_GATE_EVIDENCE_REF="$GENERATION_GATE_REF" \
BOOTSTRAP_ADMIN_TOKEN_FILE=/root/game-staging/credentials/bootstrap-admin.token \
SSH_BIN="$TMP_DIR/fake-ssh" \
bash "$ACTIVATE" >"$TMP_DIR/activate-wrapper.log"
@ -55,15 +56,60 @@ rg -q 'start_stack.*48080 4173 4174 8300 9501 true' "$TMP_DIR/remote-script.sh"
rg -q 'mv -Tf.*active_pointer' "$TMP_DIR/remote-script.sh" || fail "active 指针不是原子替换"
rg -Fq 'spring.cloud.nacos.discovery.enabled=${discovery_enabled}' "$TMP_DIR/remote-script.sh" || \
fail "backend launcher 没有显式控制 Nacos discovery"
rg -q 'start_stack.*18080 14173 14174 18300 19501 false' "$TMP_DIR/remote-script.sh" || \
fail "候选栈没有关闭 Nacos discovery"
rg -q 'start_candidate_probe.*14173 14174 18300 19501' "$TMP_DIR/remote-script.sh" || \
fail "候选非数据面探针未启动"
rg -q 'verify_generation_gate_evidence' "$TMP_DIR/remote-script.sh" || fail "缺生成 R1 证据校验"
! rg -q 'git .*cat-file.*expected_commit' "$TMP_DIR/remote-script.sh" || fail "R1 attestation 仍要求属于被证明 commit存在自引用"
rg -q 'attestation_root' "$TMP_DIR/remote-script.sh" || fail "缺独立 R1 attestation 根目录"
rg -q 'flock -n' "$TMP_DIR/remote-script.sh" || fail "activation 缺单实例部署锁"
rg -q 'candidate_backend=SKIPPED_NO_ISOLATED_DATA_PLANE' "$TMP_DIR/remote-script.sh" || \
fail "候选探活没有明确跳过共享数据面 backend"
rg -q 'run_flyway_migrations' "$TMP_DIR/remote-script.sh" || fail "迁移未拆成独立受控步骤"
rg -q 'restore_database_snapshot' "$TMP_DIR/remote-script.sh" || fail "迁移后失败缺数据库快照补偿"
rg -q 'FIRST_RC_FULL_ROLLBACK=UNAVAILABLE' "$TMP_DIR/remote-script.sh" || fail "首个 RC 未明确禁止宣称完整回滚"
rg -q 'manage-dogfood-reviewers\.py' "$TMP_DIR/remote-script.sh" || fail "激活未调用审核账号轮换工具"
rg -q 'provision-dogfood-with-nacos-transaction\.sh' "$TMP_DIR/remote-script.sh" || \
fail "激活未在 admin 禁用前执行账号预置事务"
rg -q 'ADMIN_TOKEN_FILE="\$bootstrap_admin_token_file"' "$TMP_DIR/remote-script.sh" || \
fail "激活账号预置未使用 bootstrap token 文件"
rg -q 'PROVISION_PASS accounts=40 creators=10 role_mismatches=0 claimed_accounts=40 duplicate_claims=0' \
"$TMP_DIR/remote-script.sh" || fail "激活未核对 40 人账号终验"
rg -q '请先运行受控 new-api import' "$TMP_DIR/remote-script.sh" || \
fail "额度池不足没有给出受控 new-api import 指引"
rg -q -- '--ack ROTATE_DOGFOOD_REVIEWERS' "$TMP_DIR/remote-script.sh" || fail "审核账号轮换 ACK 不精确"
rg -q 'reviewer-runtime-verification\.json' "$TMP_DIR/remote-script.sh" || fail "缺 reviewer/readonly 登录与旧 token 失效运行态证据"
rg -q 'rm -f.*bootstrap_admin_token_file' "$TMP_DIR/remote-script.sh" || fail "轮换成功后未删除 bootstrap token 文件"
! rg -q 'admin123' "$ACTIVATE" || fail "激活脚本写入了默认公开密码"
for required_marker in actor_prompt_eval judge_prompt_eval game_content_gold live_prompt full_gate fresh_25 historical_11 production_shadow_20 runtime_eligible; do
rg -q "$required_marker" "$TMP_DIR/remote-script.sh" || fail "生成门未核对 R1 字段:$required_marker"
done
gate_line="$(rg -n 'verify_generation_gate_evidence' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
snapshot_line="$(rg -n '迁移前数据库快照' "$TMP_DIR/remote-script.sh" | cut -d: -f1)"
snapshot_line="$(rg -n '迁移前数据库快照' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
[ "$gate_line" -lt "$snapshot_line" ] || fail "生成门校验没有发生在数据库快照/迁移前"
candidate_probe_line="$(rg -n '启动不接共享数据面的候选探针' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
stop_old_line="$(rg -n '候选非数据面探针全绿,停止旧栈' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
restore_verify_line="$(rg -n '在隔离临时 MySQL 容器中验证快照可恢复' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
migration_line="$(rg -n '^run_flyway_migrations$' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
[ "$candidate_probe_line" -lt "$stop_old_line" ] || fail "候选非数据面探针没有在停止旧栈前完成"
[ "$stop_old_line" -lt "$snapshot_line" ] || fail "迁移快照前旧栈仍可能写入,失败补偿会丢数据"
[ "$snapshot_line" -lt "$restore_verify_line" ] || fail "隔离恢复验证没有使用停写后的最终快照"
[ "$restore_verify_line" -lt "$migration_line" ] || fail "未先完成隔离恢复验证就执行正式迁移"
reviewer_line="$(rg -n 'manage-dogfood-reviewers\.py' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
provision_line="$(rg -n 'provision-dogfood-with-nacos-transaction\.sh' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
provision_pass_line="$(rg -n 'PROVISION_PASS accounts=40 creators=10 role_mismatches=0 claimed_accounts=40 duplicate_claims=0' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
runtime_verify_line="$(rg -n '狗粮审核账号轮换与运行态验证 PASS' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
stable_smoke_line="$(rg -n 'stable-smoke\.log' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
active_switch_line="$(rg -n '正式栈全绿,原子切换 active 指针' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
[ "$provision_line" -lt "$provision_pass_line" ] || fail "账号预置后未核对终验"
[ "$provision_pass_line" -lt "$reviewer_line" ] || fail "admin 禁用发生在账号预置恢复前"
[ "$reviewer_line" -lt "$runtime_verify_line" ] || fail "审核账号轮换后未做运行态验证"
[ "$runtime_verify_line" -lt "$stable_smoke_line" ] || fail "审核账号运行态验证没有位于写 smoke 前"
[ "$stable_smoke_line" -lt "$active_switch_line" ] || fail "active 指针在 smoke 前切换"
token_delete_line="$(rg -n '^delete_bootstrap_token$' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
[ "$active_switch_line" -lt "$token_delete_line" ] || fail "bootstrap token 未在 smoke+active 后最后删除"
failure_handler_body="$(sed -n '/^failure_handler()/,/^}/p' "$TMP_DIR/remote-script.sh")"
! printf '%s\n' "$failure_handler_body" | rg -q '^ delete_bootstrap_token' || \
fail "激活失败路径删除了补偿后重试所需的 bootstrap token"
if rg -q '/root/games-development-ai' "$TMP_DIR/remote-script.sh"; then
fail "激活脚本引用了远端脏旧仓"
fi
@ -88,6 +134,14 @@ if RC_COMMIT="0000000000000000000000000000000000000000" \
fi
[ ! -s "$TMP_DIR/ssh-called" ] || fail "非法生成门路径后仍发生 SSH"
# 真激活缺 bootstrap token 文件路径时必须在 SSH 前 fail-closed不能回退仓库默认密码。
if RC_COMMIT="0000000000000000000000000000000000000000" \
GENERATION_GATE_EVIDENCE_REF="$GENERATION_GATE_REF" SSH_BIN="$TMP_DIR/reject-ssh" \
bash "$ACTIVATE" >"$TMP_DIR/missing-bootstrap-token.log" 2>&1; then
fail "缺 BOOTSTRAP_ADMIN_TOKEN_FILE 仍允许激活"
fi
[ ! -s "$TMP_DIR/ssh-called" ] || fail "缺 bootstrap token 路径后仍发生 SSH"
# 旧变量仅允许受控路径和 hash任意 URL 必须在 SSH 前失败。
if RC_COMMIT="0000000000000000000000000000000000000000" \
GENERATION_GATE_EVIDENCE_REF="$GENERATION_GATE_REF" \
@ -96,17 +150,92 @@ if RC_COMMIT="0000000000000000000000000000000000000000" \
fail "任意 OBJECT_SNAPSHOT_REF 仍允许激活"
fi
# prepare 必须在 ready 前运行本任务全部 shell 回归,并明确记录跳过的后端测试
# prepare 必须在 ready 前运行发布工具回归与安全关键 Java 测试,并把日志哈希绑定 RC commit
for test_name in test-staging-rc.sh test-provision-dogfood-users.sh \
test-provision-dogfood-nacos-transaction.sh test-import-newapi-pool.sh; do
test-provision-dogfood-nacos-transaction.sh test-import-newapi-pool.sh \
test-dogfood-smoke.sh; do
rg -q "$test_name" "$PREPARE" || fail "prepare 未运行发布工具回归:$test_name"
done
rg -q 'release-tool-tests\.log' "$PREPARE" || fail "prepare 未保存发布工具测试日志"
rg -q 'backend_tests=NOT_RUN' "$PREPARE" || fail "prepare 未诚实记录后端测试未运行"
rg -q 'security-java-tests\.log' "$PREPARE" || fail "prepare 未保存安全关键 Java 测试日志"
rg -q 'DogfoodStartupValidatorTest' "$PREPARE" || fail "prepare 未运行 Dogfood 启动安全测试"
rg -q 'DogfoodSecurityStartupValidatorTest' "$PREPARE" || fail "prepare 未运行 Dogfood 配置安全测试"
rg -q 'TokenAuthenticationFilterTest' "$PREPARE" || fail "prepare 未运行令牌认证过滤器测试"
rg -q 'AuthenticationEntryPointImplTest' "$PREPARE" || fail "prepare 未运行未认证响应测试"
rg -q 'RuntimePackageServiceImplTest' "$PREPARE" || fail "prepare 未运行 artifact CAS 测试"
rg -q 'RuntimePackageApiImplDogfoodTest' "$PREPARE" || fail "prepare 未运行 runtime RPC 安全测试"
rg -q 'AdminNewapiQuotaControllerTest' "$PREPARE" || fail "prepare 未运行配额修复入口测试"
rg -q 'NewapiQuotaClaimConsumerTest' "$PREPARE" || fail "prepare 未运行配额 MQ 消费测试"
rg -q 'NewapiQuotaServiceImplTest' "$PREPARE" || fail "prepare 未运行配额服务测试"
rg -q 'test-dogfood-reviewer-security\.py' "$PREPARE" || fail "prepare 未运行审核账号安全测试"
rg -q 'test_dogfood_ops\.py' "$PREPARE" || fail "prepare 未运行波次证据账本测试"
rg -q 'backend_security_tests=PASS' "$PREPARE" || fail "prepare 未把安全 Java 测试结果绑定 RC"
rg -q 'security_java_tests_sha256=' "$PREPARE" || fail "prepare 摘要未绑定安全测试日志哈希"
rg -q 'verify_security_java_reports' "$PREPARE" || fail "prepare 未校验安全测试报告实际包含全部目标类"
rg -q 'security-java-tests-summary\.tsv' "$PREPARE" || fail "prepare 未保存可审计的 Java 测试计数摘要"
rg -q 'security_java_tests_summary_sha256=' "$PREPARE" || fail "prepare 摘要未绑定 Java 测试计数证据"
! rg -q 'backend_tests=NOT_RUN' "$PREPARE" || fail "prepare 仍宣称后端测试未运行"
venv_line="$(rg -n '准备 commit 内独立 Python 虚拟环境' "$PREPARE" | cut -d: -f1)"
tool_test_line="$(rg -n '运行发布工具回归测试' "$PREPARE" | cut -d: -f1)"
[ "$venv_line" -lt "$tool_test_line" ] || fail "发布工具回归未使用 commit 内已建 venv"
rg -q 'cheap-worker/\.venv/bin.*PATH' "$PREPARE" || fail "发布工具回归未显式使用 commit 内 Python"
rg -q '原子切换 active' "$PREPARE" || fail "prepare 完成提示仍未使用 active 指针"
# 独立 attestation 不属于被证明 commit内容寻址文件应通过宽权限、symlink、commit 漂移必须拒绝。
cat >"$TMP_DIR/exec-ssh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
while [ "$#" -gt 0 ] && [ "$1" != bash ]; do shift; done
[ "${1:-}" = bash ] || exit 97
exec "$@"
SH
chmod +x "$TMP_DIR/exec-ssh"
attestation_root="$(realpath "$TMP_DIR")/attestations"
mkdir -p "$attestation_root/r1"
attestation_file="$attestation_root/r1/r1.attestation"
cat >"$attestation_file" <<EOF
schema=generation-r1-gate/1
rc_commit=0000000000000000000000000000000000000000
r1_status=PASS
actor_prompt_eval=PASS
judge_prompt_eval=PASS
game_content_gold=PASS
live_prompt=PASS
full_gate=PASS
fresh_25=PASS
historical_11=PASS
production_shadow_20=PASS
runtime_eligible=true
EOF
chmod 600 "$attestation_file"
attestation_hash="$(sha256sum "$attestation_file" | awk '{print $1}')"
mv "$attestation_file" "$attestation_root/r1/$attestation_hash.attestation"
attestation_file="$attestation_root/r1/$attestation_hash.attestation"
attestation_ref="sha256:$attestation_hash:r1/$attestation_hash.attestation"
RC_COMMIT=0000000000000000000000000000000000000000 \
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >"$TMP_DIR/attestation-pass.log"
rg -q 'GENERATION_ATTESTATION_PASS' "$TMP_DIR/attestation-pass.log" || fail "独立 attestation 未通过"
chmod 644 "$attestation_file"
if RC_COMMIT=0000000000000000000000000000000000000000 \
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
fail "权限过宽 attestation 未拒绝"
fi
chmod 600 "$attestation_file"
ln -s "$attestation_file" "$attestation_root/r1/symlink.attestation"
if RC_COMMIT=0000000000000000000000000000000000000000 \
GENERATION_GATE_EVIDENCE_REF="sha256:$attestation_hash:r1/symlink.attestation" \
GENERATION_ATTESTATION_ROOT="$attestation_root" SSH_BIN="$TMP_DIR/exec-ssh" \
bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
fail "symlink attestation 未拒绝"
fi
if RC_COMMIT=1111111111111111111111111111111111111111 \
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
fail "rc_commit 漂移 attestation 未拒绝"
fi
# 直接验证 DB-only 审计:生成、外部引用 fail-closed、旧 hash/路径校验。
FAKE_RC="$TMP_DIR/fake-rc"
@ -267,7 +396,7 @@ export RC_TEST_PORT_FILE="$TMP_DIR/port"
export RC_TEST_METHOD_FILE="$TMP_DIR/methods"
python3 "$TMP_DIR/fake-readiness.py" &
SERVER_PID=$!
for _ in $(seq 1 50); do
for _ in $(seq 1 300); do
[ -s "$TMP_DIR/port" ] && break
sleep 0.1
done