267 lines
12 KiB
Bash
Executable File
267 lines
12 KiB
Bash
Executable File
#!/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 RC:host=%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
|