games-development-ai/deploy/prepare-staging-rc.sh

267 lines
12 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# 在 mini-desktop 的全新目录准备一个 commit-pinned staging RC。
# 本脚本只克隆、构建和留证,不切换 active、不停止或启动任何服务。
set -euo pipefail
RC_COMMIT="${RC_COMMIT:-}"
RC_HOST="${RC_HOST:-root@100.64.0.7}"
RC_ROOT="${RC_ROOT:-/root/game-staging/releases}"
RC_REPO_URL="${RC_REPO_URL:-$(git remote get-url origin 2>/dev/null || true)}"
if [[ ! "$RC_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then
printf 'RC_COMMIT 必须是完整 40 位小写提交哈希。\n' >&2
exit 2
fi
if [ -z "$RC_REPO_URL" ]; then
printf '无法解析仓库地址,请显式设置 RC_REPO_URL。\n' >&2
exit 2
fi
printf '准备 staging RChost=%s commit=%s\n' "$RC_HOST" "$RC_COMMIT"
printf '注意:只构建候选,不切流、不起服。\n'
# 参数经 bash 位置参数传入;远端脚本使用单引号 heredoc避免本机提前展开。
ssh -o ProxyCommand=none "$RC_HOST" bash -s -- "$RC_COMMIT" "$RC_ROOT" "$RC_REPO_URL" <<'REMOTE_SCRIPT'
set -euo pipefail
commit="$1"
root="$2"
repo_url="$3"
release="$root/$commit"
evidence="$release/evidence"
status_file="$release/RC_STATUS"
if [ -e "$release" ]; then
printf '拒绝复用既有 RC 目录:%s\n' "$release" >&2
exit 3
fi
mkdir -p "$root"
umask 077
git clone --no-checkout "$repo_url" "$release"
printf 'preparing\n' >"$status_file"
on_error() {
code=$?
printf 'failed exit=%s at=%s\n' "$code" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >"$status_file"
printf 'RC 准备失败,现场保留用于排障:%s\n' "$release" >&2
exit "$code"
}
trap on_error ERR
cd "$release"
git fetch --no-tags origin "$commit"
git checkout --detach "$commit"
actual="$(git rev-parse HEAD)"
[ "$actual" = "$commit" ] || { printf '提交不一致expected=%s actual=%s\n' "$commit" "$actual" >&2; exit 4; }
[ -z "$(git status --porcelain --untracked-files=no)" ] || { printf '新检出存在 tracked 脏改动,拒绝构建。\n' >&2; exit 4; }
mkdir -p "$evidence"
previous="none"
if [ -L "$root/active" ]; then
previous="$(readlink -f "$root/active" || true)"
[ -n "$previous" ] || previous="broken-active-symlink"
elif [ -L "$root/current" ]; then
# 兼容 active 指针启用前的旧部署;新激活脚本只维护 canonical active。
previous="$(readlink -f "$root/current" || true)"
[ -n "$previous" ] || previous="broken-current-symlink"
fi
printf '%s\n' "$previous" >"$evidence/rollback-target.txt"
# 配置只记录来源、文件哈希和键名,绝不复制或输出密钥值。
{
printf 'captured_at=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
printf 'rc_commit=%s\n' "$commit"
printf 'previous_release=%s\n' "$previous"
if [ -f /root/huijing-dev.env ]; then
printf 'huijing_dev_env_sha256=%s\n' "$(sha256sum /root/huijing-dev.env | awk '{print $1}')"
printf 'huijing_dev_env_keys='
awk -F= '/^[A-Za-z_][A-Za-z0-9_]*=/{print $1}' /root/huijing-dev.env | sort | paste -sd, -
else
printf 'huijing_dev_env=missing\n'
fi
printf 'backend_staging_yaml_sha256=%s\n' "$(sha256sum game-cloud/huijing-server/src/main/resources/application-staging.yaml | awk '{print $1}')"
printf 'studio_staging_env_sha256=%s\n' "$(sha256sum game-studio/.env.staging | awk '{print $1}')"
printf 'admin_stage_env_sha256=%s\n' "$(sha256sum game-admin/.env.stage | awk '{print $1}')"
} >"$evidence/config-snapshot.txt"
printf '[1/7] 构建 Java 后端(构建阶段跳过全量测试,下一步单独执行安全关键集)\n'
(cd game-cloud && mvn clean install -pl huijing-server -am -DskipTests)
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():
executed=tests-skipped
if tests < 1 or executed < 1 or failures or errors:
raise SystemExit(
f"安全测试未通过: {name} tests={tests} skipped={skipped} "
f"executed={executed} 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 '[4/7] 构建 game-admin stage\n'
corepack prepare pnpm@9.15.4 --activate
(cd game-admin && pnpm install --frozen-lockfile && pnpm build:stage)
# 前端制品可被所有内测用户读取,任何默认口令、测试口令、模拟令牌或预置密钥标识都必须拒绝签发。
frontend_forbidden_patterns='admin123|test1|mock-token|mock-studio-token|bootstrap-admin\.token|DOGFOOD_PASSWORD_SEED'
frontend_forbidden_patterns_sha256="$(printf '%s' "$frontend_forbidden_patterns" | sha256sum | awk '{print $1}')"
{
printf 'schema=frontend-secret-scan/1\n'
printf 'rc_commit=%s\n' "$commit"
printf 'build_dirs=game-studio/dist,game-admin/dist-stage\n'
printf 'patterns=%s\n' "$frontend_forbidden_patterns"
printf 'patterns_sha256=%s\n' "$frontend_forbidden_patterns_sha256"
} >"$evidence/frontend-secret-scan.log"
for build_dir in game-studio/dist game-admin/dist-stage; do
if rg -n -i "$frontend_forbidden_patterns" "$build_dir" >>"$evidence/frontend-secret-scan.log"; then
printf 'status=FAIL\n' >>"$evidence/frontend-secret-scan.log"
printf '前端制品包含禁止发布的凭据或模拟令牌:%s\n' "$build_dir" >&2
exit 5
fi
done
printf 'status=PASS\nFRONTEND_SECRET_SCAN_PASS\n' >>"$evidence/frontend-secret-scan.log"
chmod 600 "$evidence/frontend-secret-scan.log"
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 '[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"
for test_script in "${release_tool_tests[@]}"; do
printf 'RUN %s\n' "$test_script"
bash "$test_script"
done
printf 'RUN game-runtime/tools/test_newapi_pool_provision.py\n'
python3 -m unittest game-runtime/tools/test_newapi_pool_provision.py
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 'RUN deploy/tests/test-runtime-tree-manifest.py\n'
python3 -m unittest deploy/tests/test-runtime-tree-manifest.py
printf 'RELEASE_TOOL_TESTS_PASS shell=%s python=4\n' "${#release_tool_tests[@]}"
) >"$evidence/release-tool-tests.log" 2>&1
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"
printf 'admin_dist=%s/game-admin/dist-stage\n' "$release"
printf 'cheap_agent_python=%s/cheap-worker/.venv/bin/python\n' "$release"
printf 'cheap_agent_entry=%s/cheap-worker/cheap_service_app.py\n' "$release"
printf 'cheap_worker_entry=%s/cheap-worker/worker_service.py\n' "$release"
printf 'tier2_python=%s/tier2/gen-worker/.venv/bin/python\n' "$release"
printf 'tier2_entry=%s/tier2/gen-worker/service/app.py\n' "$release"
} >"$evidence/artifact-paths.env"
python3 deploy/runtime-tree-manifest.py capture \
--output "$evidence/runtime-tree.json" cheap-worker tier2/gen-worker \
>"$evidence/runtime-tree-capture.log"
chmod 600 "$evidence/runtime-tree.json" "$evidence/runtime-tree-capture.log"
{
sha256sum game-cloud/huijing-server/target/huijing-server.jar
find game-studio/dist game-admin/dist-stage -type f -print0 | sort -z | xargs -0 sha256sum
sha256sum "$evidence/runtime-tree.json"
} >"$evidence/artifacts.sha256"
{
printf 'schema=staging-rc-prepare-verification/1\n'
printf 'rc_commit=%s\n' "$commit"
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_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'
printf 'frontend_secret_scan=PASS\n'
printf 'frontend_secret_scan_patterns_sha256=%s\n' "$frontend_forbidden_patterns_sha256"
printf 'frontend_secret_scan_sha256=%s\n' "$(sha256sum "$evidence/frontend-secret-scan.log" | awk '{print $1}')"
printf 'runtime_tree_manifest_sha256=%s\n' "$(sha256sum "$evidence/runtime-tree.json" | awk '{print $1}')"
} >"$evidence/verification-summary.txt"
[ -z "$(git status --porcelain --untracked-files=no)" ] || { printf '构建修改了 tracked 文件,拒绝将其标为 ready。\n' >&2; exit 6; }
printf 'ready commit=%s built_at=%s\n' "$commit" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >"$status_file"
chmod -R go-rwx "$evidence" "$status_file"
trap - ERR
printf 'RC_PREPARE_PASS release=%s\n' "$release"
printf 'rollback_target=%s\n' "$previous"
printf '下一步由统一部署窗口读取 evidence/artifact-paths.env探活通过后再原子切换 active。\n'
REMOTE_SCRIPT