games-development-ai/deploy/generate-staging-storage-audit.sh

117 lines
5.5 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
# 为 commit-pinned staging RC 生成可信存储审计。
# 当前狗粮主链只允许 DB 内嵌产物;一旦发现外部 URL 或 infra 文件行,立即拒绝把对象存储标为 N/A。
set -euo pipefail
RC_RELEASE="${RC_RELEASE:-}"
MYSQL_CONTAINER="${MYSQL_CONTAINER:-game-staging-mysql}"
MYSQL_DATABASE="${MYSQL_DATABASE:-ruoyi-vue-pro}"
STORAGE_AUDIT_MYSQL_BIN="${STORAGE_AUDIT_MYSQL_BIN:-}"
EXPECTED_STORAGE_AUDIT_REF="${EXPECTED_STORAGE_AUDIT_REF:-}"
if [ -z "$RC_RELEASE" ] || [ ! -d "$RC_RELEASE/evidence" ]; then
printf 'STORAGE_AUDIT_FAIL RC_RELEASE 必须指向已准备且含 evidence 的 RC。\n' >&2
exit 2
fi
# 输出位置由脚本固定,调用方不能把审计引用引向 RC 之外的任意文件。
audit_file="$RC_RELEASE/evidence/storage-audit.tsv"
audit_tmp="$audit_file.tmp.$$"
trap 'rm -f "$audit_tmp"' EXIT
# 兼容旧调用方时,先校验它引用的是 RC 内由本脚本产生的既有审计URL、绝对路径和错误 hash 都不能通过。
if [ -n "$EXPECTED_STORAGE_AUDIT_REF" ]; then
if [[ ! "$EXPECTED_STORAGE_AUDIT_REF" =~ ^sha256:[0-9a-f]{64}:evidence/storage-audit\.tsv$ ]]; then
printf 'STORAGE_AUDIT_FAIL 旧快照引用格式或路径不受控。\n' >&2
exit 5
fi
[ -s "$audit_file" ] || {
printf 'STORAGE_AUDIT_FAIL 旧快照引用指向的 RC 审计不存在。\n' >&2
exit 5
}
expected_hash="${EXPECTED_STORAGE_AUDIT_REF#sha256:}"
expected_hash="${expected_hash%%:*}"
existing_hash="$(sha256sum "$audit_file" | awk '{print $1}')"
[ "$expected_hash" = "$existing_hash" ] || {
printf 'STORAGE_AUDIT_FAIL 调用方提供的 hash 与 RC 既有审计不一致。\n' >&2
exit 5
}
fi
run_mysql_query() {
if [ -n "$STORAGE_AUDIT_MYSQL_BIN" ]; then
"$STORAGE_AUDIT_MYSQL_BIN"
return
fi
docker inspect "$MYSQL_CONTAINER" >/dev/null
# `-i` 必须保留:审计 SQL 从本脚本 heredoc 经 stdin 送入容器内 mysql。
docker exec -i "$MYSQL_CONTAINER" sh -lc \
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -uroot -N -B "$1"' sh "$MYSQL_DATABASE"
}
value_of() {
local key="$1" file="$2"
awk -F= -v wanted="$key" '$1 == wanted {print substr($0, length($1) + 2); found=1} END {exit !found}' "$file"
}
{
printf 'schema=staging-storage-audit/1\n'
printf 'captured_at=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
printf 'rc_commit=%s\n' "$(git -C "$RC_RELEASE" rev-parse HEAD)"
run_mysql_query <<'SQL'
SELECT CONCAT('game_version_rows=', COUNT(*)) FROM game_version WHERE deleted=0;
SELECT CONCAT('game_version_package_url_nonempty=', COALESCE(SUM(package_url<>''),0)) FROM game_version WHERE deleted=0;
SELECT CONCAT('game_runtime_package_rows=', COUNT(*)) FROM game_runtime_package WHERE deleted=0;
SELECT CONCAT('game_runtime_package_url_nonempty=', COALESCE(SUM(package_url<>''),0)) FROM game_runtime_package WHERE deleted=0;
SELECT CONCAT('game_runtime_package_payload_rows=', COALESCE(SUM(package_json IS NOT NULL AND CHAR_LENGTH(package_json)>0),0)) FROM game_runtime_package WHERE deleted=0;
SELECT CONCAT('game_source_project_rows=', COUNT(*)) FROM game_source_project WHERE deleted=0;
SELECT CONCAT('game_source_project_url_nonempty=', COALESCE(SUM(source_url<>''),0)) FROM game_source_project WHERE deleted=0;
SELECT CONCAT('game_source_project_payload_rows=', COALESCE(SUM(source_json IS NOT NULL AND CHAR_LENGTH(source_json)>0),0)) FROM game_source_project WHERE deleted=0;
SELECT CONCAT('game_aigc_task_rows=', COUNT(*)) FROM game_aigc_task WHERE deleted=0;
SELECT CONCAT('game_aigc_result_package_url_nonempty=', COALESCE(SUM(result_package_url<>''),0)) FROM game_aigc_task WHERE deleted=0;
SELECT CONCAT('infra_file_rows=', COUNT(*)) FROM infra_file WHERE deleted=0;
SQL
} >"$audit_tmp"
required_numeric_keys='game_version_rows game_version_package_url_nonempty game_runtime_package_rows game_runtime_package_url_nonempty game_runtime_package_payload_rows game_source_project_rows game_source_project_url_nonempty game_source_project_payload_rows game_aigc_task_rows game_aigc_result_package_url_nonempty infra_file_rows'
for key in $required_numeric_keys; do
value="$(value_of "$key" "$audit_tmp")" || {
printf 'STORAGE_AUDIT_FAIL 缺少审计字段:%s\n' "$key" >&2
exit 3
}
[[ "$value" =~ ^[0-9]+$ ]] || {
printf 'STORAGE_AUDIT_FAIL 审计字段不是非负整数:%s\n' "$key" >&2
exit 3
}
done
# 任一外部引用或 infra 文件都意味着存在尚未纳入快照的对象,不能继续使用 DB_ONLY_NA。
external_total=$((
$(value_of game_version_package_url_nonempty "$audit_tmp") +
$(value_of game_runtime_package_url_nonempty "$audit_tmp") +
$(value_of game_source_project_url_nonempty "$audit_tmp") +
$(value_of game_aigc_result_package_url_nonempty "$audit_tmp") +
$(value_of infra_file_rows "$audit_tmp")
))
if [ "$external_total" -ne 0 ]; then
printf 'STORAGE_AUDIT_FAIL 发现 %s 个外部存储引用/文件行,必须先实现对应 MinIO 前缀快照。\n' \
"$external_total" >&2
exit 4
fi
{
cat "$audit_tmp"
printf 'object_storage_status=DB_ONLY_NA\n'
printf 'minio_involved_prefix_count=0\n'
printf 'minio_involved_prefixes=DB_ONLY_NA\n'
printf 'minio_scope_reason=no_external_package_or_source_url_and_no_infra_file_rows\n'
} >"$audit_file"
chmod 600 "$audit_file"
audit_hash="$(sha256sum "$audit_file" | awk '{print $1}')"
audit_ref="sha256:${audit_hash}:evidence/storage-audit.tsv"
printf '%s\n' "$audit_ref" >"$RC_RELEASE/evidence/storage-audit.ref"
chmod 600 "$RC_RELEASE/evidence/storage-audit.ref"
printf 'STORAGE_AUDIT_PASS ref=%s\n' "$audit_ref"