Files
Genarrative/scripts/build-production-release.sh
kdletters e3258f6f1d 完善 Pingora 直连切换门禁
新增 Pingora shadow env 回切脚本与对应检查。

补齐直连证据包时间线和 cutoverRunId 审计门禁。

支持 Gitea Host 透传并更新直连多域名文档。

修复百分号编码静态图标路径并补 smoke 覆盖。

更新生产发布与运维护栏对 Pingora 发布包的校验。
2026-06-18 21:10:45 +08:00

613 lines
24 KiB
Bash
Raw Permalink 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
set -euo pipefail
usage() {
cat <<'EOF'
用法:
npm run build:production-release -- --name <version>
说明:
1. 生成生产发布包 build/<version>/。
2. 发布包只包含 systemd + Nginx 生产部署需要的文件,不再生成旧 start.sh / stop.sh / web-server.mjs。
3. 默认会构建主站、后台、api-server 与 SpacetimeDB wasm;调试时可用 skip 参数复用已存在产物。
参数:
--name <version> 指定 build 子目录名,默认使用当前时间 YYYYmmdd-HHMMSS
--component <name> 构建组件: all, web, api-server, spacetime-module;默认 all
--skip-web-build 跳过主站与后台构建,仅复制已有 dist 产物
--skip-api-build 跳过 api-server 构建,仅复制已有 release 二进制
--skip-spacetime-build 跳过 spacetime-module 构建,仅复制已有 wasm
--include-pingora-gateway
额外构建并打包 Pingora 影子网关二进制,默认不启用
--skip-pingora-gateway-build
跳过 Pingora 构建,仅复制已有 release 二进制
--no-migration-bootstrap-secret
构建不带迁移引导密钥的 spacetime-module wasm
EOF
}
require_command() {
local command_name="$1"
if ! command -v "${command_name}" >/dev/null 2>&1; then
echo "[production-release] 缺少命令: ${command_name}" >&2
exit 1
fi
}
require_any_command() {
local label="$1"
shift
local command_name
for command_name in "$@"; do
if command -v "${command_name}" >/dev/null 2>&1; then
return
fi
done
echo "[production-release] 缺少 ${label}: $*" >&2
exit 1
}
require_pingora_build_toolchain() {
require_command cargo
require_command cmake
require_any_command "C 编译器" cc clang gcc
require_any_command "C++ 编译器" c++ clang++ g++
}
copy_required_file() {
local source_path="$1"
local target_path="$2"
local label="$3"
if [[ ! -f "${source_path}" ]]; then
echo "[production-release] 缺少 ${label}: ${source_path}" >&2
exit 1
fi
cp "${source_path}" "${target_path}"
}
copy_required_dir() {
local source_path="$1"
local target_path="$2"
local label="$3"
if [[ ! -d "${source_path}" ]]; then
echo "[production-release] 缺少 ${label}: ${source_path}" >&2
exit 1
fi
mkdir -p "$(dirname "${target_path}")"
rm -rf "${target_path}"
cp -R "${source_path}" "${target_path}"
}
write_sha256_file() {
local file_path="$1"
local checksum_path="${file_path}.sha256"
if [[ ! -f "${file_path}" ]]; then
echo "[production-release] 无法生成 checksum,文件不存在: ${file_path}" >&2
exit 1
fi
(
cd "$(dirname "${file_path}")"
sha256sum "$(basename "${file_path}")"
) >"${checksum_path}"
}
generate_migration_bootstrap_secret() {
node -e 'const crypto = require("crypto"); process.stdout.write(crypto.randomBytes(32).toString("hex"));'
}
prepare_migration_bootstrap_secret() {
local secret_source="generated"
if [[ "${BUILD_SPACETIME}" -ne 1 || "${SKIP_SPACETIME_BUILD}" -eq 1 ]]; then
return
fi
if [[ "${MIGRATION_BOOTSTRAP_SECRET_MODE}" == "disabled" ]]; then
unset GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET
echo "[production-release] 未启用迁移引导密钥。"
return
fi
if [[ -n "${GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET:-}" ]]; then
MIGRATION_BOOTSTRAP_SECRET="${GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET}"
secret_source="environment"
else
MIGRATION_BOOTSTRAP_SECRET="$(generate_migration_bootstrap_secret)"
export GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET="${MIGRATION_BOOTSTRAP_SECRET}"
fi
if [[ "${#MIGRATION_BOOTSTRAP_SECRET}" -lt 16 ]]; then
echo "[production-release] 迁移引导密钥至少需要 16 个字符。" >&2
exit 1
fi
echo "[production-release] 已准备迁移引导密钥: source=${secret_source}, length=${#MIGRATION_BOOTSTRAP_SECRET}"
}
write_migration_bootstrap_secret_file() {
local target_path="${TARGET_DIR}/migration-bootstrap-secret.txt"
if [[ "${BUILD_SPACETIME}" -ne 1 || "${SKIP_SPACETIME_BUILD}" -eq 1 ]]; then
return
fi
if [[ "${MIGRATION_BOOTSTRAP_SECRET_MODE}" == "disabled" ]]; then
return
fi
if [[ -z "${MIGRATION_BOOTSTRAP_SECRET}" ]]; then
echo "[production-release] 迁移引导密钥为空,无法写入发布产物。" >&2
exit 1
fi
printf "%s\n" "${MIGRATION_BOOTSTRAP_SECRET}" >"${target_path}"
chmod 600 "${target_path}" 2>/dev/null || true
MIGRATION_BOOTSTRAP_SECRET_ARTIFACT=1
echo "[production-release] 已写入迁移引导密钥文件: ${target_path}"
}
write_release_manifest() {
RELEASE_MANIFEST_PATH="${TARGET_DIR}/release-manifest.json" \
RELEASE_VERSION="${BUILD_NAME}" \
RELEASE_SOURCE_BRANCH="${RELEASE_SOURCE_BRANCH}" \
RELEASE_SOURCE_COMMIT="${RELEASE_SOURCE_COMMIT}" \
RELEASE_BUILT_AT="${RELEASE_BUILT_AT}" \
RELEASE_COMPONENT="${COMPONENT}" \
RELEASE_INCLUDE_WEB="${BUILD_WEB}" \
RELEASE_INCLUDE_API="${BUILD_API}" \
RELEASE_INCLUDE_SPACETIME="${BUILD_SPACETIME}" \
RELEASE_INCLUDE_PINGORA_GATEWAY="${INCLUDE_PINGORA_GATEWAY}" \
RELEASE_INCLUDE_MIGRATION_BOOTSTRAP_SECRET="${MIGRATION_BOOTSTRAP_SECRET_ARTIFACT}" \
node <<'NODE'
const fs = require('fs');
const artifacts = [];
if (process.env.RELEASE_INCLUDE_WEB === '1') {
artifacts.push({
component: 'web',
path: 'web.tar.gz',
checksum_path: 'web.tar.gz.sha256',
});
}
if (process.env.RELEASE_INCLUDE_API === '1') {
artifacts.push({
component: 'api-server',
path: 'api-server',
checksum_path: 'api-server.sha256',
});
}
if (process.env.RELEASE_INCLUDE_SPACETIME === '1') {
artifacts.push({
component: 'spacetime-module',
path: 'spacetime_module.wasm',
checksum_path: 'spacetime_module.wasm.sha256',
});
}
if (process.env.RELEASE_INCLUDE_PINGORA_GATEWAY === '1') {
artifacts.push({
component: 'pingora-gateway',
path: 'pingora-gateway',
checksum_path: 'pingora-gateway.sha256',
});
}
if (process.env.RELEASE_INCLUDE_MIGRATION_BOOTSTRAP_SECRET === '1') {
artifacts.push({
component: 'spacetime-module',
path: 'migration-bootstrap-secret.txt',
sensitive: true,
});
}
const manifest = {
version: process.env.RELEASE_VERSION,
source_branch: process.env.RELEASE_SOURCE_BRANCH,
source_commit: process.env.RELEASE_SOURCE_COMMIT,
built_at: process.env.RELEASE_BUILT_AT,
component_type: process.env.RELEASE_COMPONENT,
artifacts,
};
fs.writeFileSync(process.env.RELEASE_MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}\n`);
NODE
}
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
SERVER_RS_DIR="${REPO_ROOT}/server-rs"
BUILD_ROOT="${REPO_ROOT}/build"
BUILD_NAME="$(date +%Y%m%d-%H%M%S)"
COMPONENT="all"
SKIP_WEB_BUILD=0
SKIP_API_BUILD=0
SKIP_SPACETIME_BUILD=0
SKIP_PINGORA_GATEWAY_BUILD=0
INCLUDE_PINGORA_GATEWAY=0
MIGRATION_BOOTSTRAP_SECRET=""
MIGRATION_BOOTSTRAP_SECRET_ARTIFACT=0
MIGRATION_BOOTSTRAP_SECRET_MODE="auto"
BUILD_COMPLETED=0
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
--name)
BUILD_NAME="${2:?缺少 --name 的值}"
shift 2
;;
--component)
COMPONENT="${2:?缺少 --component 的值}"
shift 2
;;
--skip-web-build)
SKIP_WEB_BUILD=1
shift
;;
--skip-api-build)
SKIP_API_BUILD=1
shift
;;
--skip-spacetime-build)
SKIP_SPACETIME_BUILD=1
shift
;;
--include-pingora-gateway)
INCLUDE_PINGORA_GATEWAY=1
shift
;;
--skip-pingora-gateway-build)
SKIP_PINGORA_GATEWAY_BUILD=1
shift
;;
--no-migration-bootstrap-secret)
MIGRATION_BOOTSTRAP_SECRET=""
MIGRATION_BOOTSTRAP_SECRET_MODE="disabled"
shift
;;
*)
echo "[production-release] 未知参数: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ ! "${BUILD_NAME}" =~ ^[0-9A-Za-z._-]+$ ]]; then
echo "[production-release] --name 只能包含数字、字母、点、下划线和短横线。" >&2
exit 1
fi
BUILD_WEB=0
BUILD_API=0
BUILD_SPACETIME=0
case "${COMPONENT}" in
all)
BUILD_WEB=1
BUILD_API=1
BUILD_SPACETIME=1
;;
web)
BUILD_WEB=1
;;
api|api-server)
COMPONENT="api-server"
BUILD_API=1
;;
stdb|spacetime|spacetime-module)
COMPONENT="spacetime-module"
BUILD_SPACETIME=1
;;
*)
echo "[production-release] --component 只能是 all, web, api-server 或 spacetime-module,当前值: ${COMPONENT}" >&2
exit 1
;;
esac
TARGET_DIR="${BUILD_ROOT}/${BUILD_NAME}"
WEB_DIR="${TARGET_DIR}/web"
ADMIN_WEB_DIR="${WEB_DIR}/admin"
CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-${SERVER_RS_DIR}/target}"
API_BINARY_SOURCE="${CARGO_TARGET_DIR}/x86_64-unknown-linux-gnu/release/api-server"
PINGORA_GATEWAY_BINARY_SOURCE="${CARGO_TARGET_DIR}/x86_64-unknown-linux-gnu/release/pingora-gateway"
WASM_SOURCE="${CARGO_TARGET_DIR}/wasm32-unknown-unknown/release/spacetime_module.wasm"
RELEASE_SOURCE_BRANCH="${SOURCE_BRANCH:-${GIT_BRANCH:-}}"
RELEASE_SOURCE_BRANCH="${RELEASE_SOURCE_BRANCH#origin/}"
RELEASE_SOURCE_COMMIT="${SOURCE_COMMIT:-${GIT_COMMIT:-}}"
RELEASE_BUILT_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
if [[ -z "${RELEASE_SOURCE_BRANCH}" ]]; then
RELEASE_SOURCE_BRANCH="$(git -C "${REPO_ROOT}" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
if [[ -z "${RELEASE_SOURCE_BRANCH}" || "${RELEASE_SOURCE_BRANCH}" == "HEAD" ]]; then
RELEASE_SOURCE_BRANCH="unknown"
fi
fi
if [[ -z "${RELEASE_SOURCE_COMMIT}" ]]; then
RELEASE_SOURCE_COMMIT="$(git -C "${REPO_ROOT}" rev-parse HEAD 2>/dev/null || true)"
if [[ -z "${RELEASE_SOURCE_COMMIT}" ]]; then
RELEASE_SOURCE_COMMIT="unknown"
fi
fi
cleanup_partial_build() {
if [[ "${BUILD_COMPLETED}" -ne 1 && -n "${TARGET_DIR:-}" && -d "${TARGET_DIR}" ]]; then
echo "[production-release] 清理未完成发布包: ${TARGET_DIR}" >&2
rm -rf "${TARGET_DIR}"
fi
}
trap cleanup_partial_build EXIT
if [[ -e "${TARGET_DIR}" ]]; then
echo "[production-release] 目标目录已存在: ${TARGET_DIR}" >&2
exit 1
fi
require_command node
require_command sha256sum
if [[ "${BUILD_API}" -eq 1 && "${SKIP_API_BUILD}" -ne 1 ]]; then
require_command cargo
fi
if [[ "${BUILD_SPACETIME}" -eq 1 && "${SKIP_SPACETIME_BUILD}" -ne 1 ]]; then
require_command cargo
fi
if [[ "${INCLUDE_PINGORA_GATEWAY}" -eq 1 && "${SKIP_PINGORA_GATEWAY_BUILD}" -ne 1 ]]; then
require_pingora_build_toolchain
fi
if [[ "${BUILD_WEB}" -eq 1 && "${SKIP_WEB_BUILD}" -ne 1 ]]; then
require_command node
require_command npm
fi
if [[ "${BUILD_WEB}" -eq 1 ]]; then
require_command tar
fi
mkdir -p "${TARGET_DIR}"
echo "[production-release] 发布包目录: ${TARGET_DIR}"
echo "[production-release] 构建组件: ${COMPONENT}"
prepare_migration_bootstrap_secret
if [[ "${BUILD_WEB}" -eq 1 ]]; then
mkdir -p "${WEB_DIR}"
if [[ "${SKIP_WEB_BUILD}" -ne 1 ]]; then
echo "[production-release] 构建主站静态资源 -> ${WEB_DIR}"
(
cd "${REPO_ROOT}"
node scripts/vite-cli.mjs build --outDir "${WEB_DIR}" --emptyOutDir
)
echo "[production-release] 构建后台静态资源 -> ${ADMIN_WEB_DIR}"
(
cd "${REPO_ROOT}"
MSYS2_ARG_CONV_EXCL="--base=" node scripts/admin-web-build.mjs build --base=/admin/ --outDir "${ADMIN_WEB_DIR}" --emptyOutDir
)
else
copy_required_dir "${REPO_ROOT}/dist" "${WEB_DIR}" "主站 dist"
copy_required_dir "${REPO_ROOT}/apps/admin-web/dist" "${ADMIN_WEB_DIR}" "后台 dist"
fi
if [[ ! -f "${WEB_DIR}/maintenance.html" ]]; then
cat >"${WEB_DIR}/maintenance.html" <<'MAINTENANCE_HTML'
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>服务维护中</title>
<style>
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
background: #101418;
color: #f3f7fb;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
main {
max-width: 28rem;
padding: 2rem;
text-align: center;
}
h1 {
margin: 0 0 0.75rem;
font-size: 1.75rem;
}
p {
margin: 0;
color: #b6c2cf;
line-height: 1.7;
}
</style>
</head>
<body>
<main>
<h1>服务维护中</h1>
<p>我们正在更新系统,稍后请重新访问。</p>
</main>
</body>
</html>
MAINTENANCE_HTML
fi
echo "[production-release] 规范 Web 静态资源权限"
find "${WEB_DIR}" -type d -exec chmod 755 {} +
find "${WEB_DIR}" -type f -exec chmod 644 {} +
echo "[production-release] 打包 Web 静态资源 -> ${TARGET_DIR}/web.tar.gz"
tar -czf "${TARGET_DIR}/web.tar.gz" -C "${TARGET_DIR}" web
write_sha256_file "${TARGET_DIR}/web.tar.gz"
fi
if [[ "${BUILD_API}" -eq 1 && "${SKIP_API_BUILD}" -ne 1 ]]; then
echo "[production-release] 构建 api-server -> x86_64-unknown-linux-gnu"
(
cd "${SERVER_RS_DIR}"
cargo build \
-p api-server \
--release \
--target x86_64-unknown-linux-gnu \
--manifest-path "${SERVER_RS_DIR}/Cargo.toml"
)
fi
if [[ "${BUILD_API}" -eq 1 ]]; then
copy_required_file "${API_BINARY_SOURCE}" "${TARGET_DIR}/api-server" "api-server release binary"
chmod +x "${TARGET_DIR}/api-server"
write_sha256_file "${TARGET_DIR}/api-server"
fi
if [[ "${INCLUDE_PINGORA_GATEWAY}" -eq 1 && "${SKIP_PINGORA_GATEWAY_BUILD}" -ne 1 ]]; then
echo "[production-release] 构建 pingora-gateway -> x86_64-unknown-linux-gnu"
(
cd "${SERVER_RS_DIR}"
cargo build \
-p pingora-gateway \
--release \
--target x86_64-unknown-linux-gnu \
--manifest-path "${SERVER_RS_DIR}/Cargo.toml"
)
fi
if [[ "${INCLUDE_PINGORA_GATEWAY}" -eq 1 ]]; then
copy_required_file "${PINGORA_GATEWAY_BINARY_SOURCE}" "${TARGET_DIR}/pingora-gateway" "pingora-gateway release binary"
chmod +x "${TARGET_DIR}/pingora-gateway"
write_sha256_file "${TARGET_DIR}/pingora-gateway"
fi
if [[ "${BUILD_SPACETIME}" -eq 1 && "${SKIP_SPACETIME_BUILD}" -ne 1 ]]; then
echo "[production-release] 构建 spacetime-module -> wasm32-unknown-unknown"
(
cd "${SERVER_RS_DIR}"
cargo build \
-p spacetime-module \
--release \
--target wasm32-unknown-unknown \
--manifest-path "${SERVER_RS_DIR}/Cargo.toml"
)
fi
if [[ "${BUILD_SPACETIME}" -eq 1 ]]; then
copy_required_file "${WASM_SOURCE}" "${TARGET_DIR}/spacetime_module.wasm" "spacetime-module wasm"
write_sha256_file "${TARGET_DIR}/spacetime_module.wasm"
write_migration_bootstrap_secret_file
fi
mkdir -p "${TARGET_DIR}/scripts" "${TARGET_DIR}/scripts/deploy" "${TARGET_DIR}/scripts/ops" "${TARGET_DIR}/deploy"
cp "${SCRIPT_DIR}/deploy/maintenance-on.sh" "${TARGET_DIR}/scripts/maintenance-on.sh"
cp "${SCRIPT_DIR}/deploy/maintenance-off.sh" "${TARGET_DIR}/scripts/maintenance-off.sh"
cp "${SCRIPT_DIR}/deploy/maintenance-status.sh" "${TARGET_DIR}/scripts/maintenance-status.sh"
cp "${SCRIPT_DIR}/deploy/production-api-deploy.sh" "${TARGET_DIR}/scripts/deploy/production-api-deploy.sh"
cp "${SCRIPT_DIR}/deploy/maintenance-on.sh" "${TARGET_DIR}/scripts/deploy/maintenance-on.sh"
cp "${SCRIPT_DIR}/deploy/maintenance-off.sh" "${TARGET_DIR}/scripts/deploy/maintenance-off.sh"
cp "${SCRIPT_DIR}/deploy/pingora-direct-enable.sh" "${TARGET_DIR}/scripts/deploy/pingora-direct-enable.sh"
cp "${SCRIPT_DIR}/deploy/pingora-direct-rollback.sh" "${TARGET_DIR}/scripts/deploy/pingora-direct-rollback.sh"
cp "${SCRIPT_DIR}/deploy/pingora-realpath-canary-enable.sh" "${TARGET_DIR}/scripts/deploy/pingora-realpath-canary-enable.sh"
cp "${SCRIPT_DIR}/deploy/pingora-realpath-canary-disable.sh" "${TARGET_DIR}/scripts/deploy/pingora-realpath-canary-disable.sh"
cp "${SCRIPT_DIR}/deploy/pingora-health-patrol-env-switch.mjs" "${TARGET_DIR}/scripts/deploy/pingora-health-patrol-env-switch.mjs"
cp "${SCRIPT_DIR}/deploy/pingora-gateway-env-shadow-switch.mjs" "${TARGET_DIR}/scripts/deploy/pingora-gateway-env-shadow-switch.mjs"
cp "${SCRIPT_DIR}/deploy/pingora-tls-cert-sync.mjs" "${TARGET_DIR}/scripts/deploy/pingora-tls-cert-sync.mjs"
cp "${SCRIPT_DIR}/deploy/jenkins-inbound-agent-start.sh" "${TARGET_DIR}/scripts/jenkins-inbound-agent-start.sh"
cp "${SCRIPT_DIR}/deploy/install-jenkins-inbound-agent.sh" "${TARGET_DIR}/scripts/install-jenkins-inbound-agent.sh"
cp "${SCRIPT_DIR}/deploy/jenkins-agent-reverse-tunnel.ps1" "${TARGET_DIR}/scripts/jenkins-agent-reverse-tunnel.ps1"
cp "${SCRIPT_DIR}/deploy/jenkins-local-controller-watchdog.ps1" "${TARGET_DIR}/scripts/jenkins-local-controller-watchdog.ps1"
chmod +x \
"${TARGET_DIR}/scripts/maintenance-on.sh" \
"${TARGET_DIR}/scripts/maintenance-off.sh" \
"${TARGET_DIR}/scripts/maintenance-status.sh" \
"${TARGET_DIR}/scripts/deploy/production-api-deploy.sh" \
"${TARGET_DIR}/scripts/deploy/maintenance-on.sh" \
"${TARGET_DIR}/scripts/deploy/maintenance-off.sh" \
"${TARGET_DIR}/scripts/deploy/pingora-direct-enable.sh" \
"${TARGET_DIR}/scripts/deploy/pingora-direct-rollback.sh" \
"${TARGET_DIR}/scripts/deploy/pingora-realpath-canary-enable.sh" \
"${TARGET_DIR}/scripts/deploy/pingora-realpath-canary-disable.sh" \
"${TARGET_DIR}/scripts/deploy/pingora-health-patrol-env-switch.mjs" \
"${TARGET_DIR}/scripts/deploy/pingora-gateway-env-shadow-switch.mjs" \
"${TARGET_DIR}/scripts/deploy/pingora-tls-cert-sync.mjs" \
"${TARGET_DIR}/scripts/jenkins-inbound-agent-start.sh" \
"${TARGET_DIR}/scripts/install-jenkins-inbound-agent.sh"
copy_required_file "${SCRIPT_DIR}/spacetime-export-migration-json.mjs" "${TARGET_DIR}/scripts/database-export.mjs" "数据库导出脚本"
copy_required_file "${SCRIPT_DIR}/spacetime-import-migration-json.mjs" "${TARGET_DIR}/scripts/database-import.mjs" "数据库导入脚本"
copy_required_file "${SCRIPT_DIR}/spacetime-migration-common.mjs" "${TARGET_DIR}/scripts/spacetime-migration-common.mjs" "数据库迁移公共脚本"
copy_required_file "${SCRIPT_DIR}/spacetime-authorize-migration-operator.mjs" "${TARGET_DIR}/scripts/spacetime-authorize-migration-operator.mjs" "数据库迁移授权脚本"
copy_required_file "${SCRIPT_DIR}/spacetime-revoke-migration-operator.mjs" "${TARGET_DIR}/scripts/spacetime-revoke-migration-operator.mjs" "数据库迁移撤权脚本"
copy_required_file "${SCRIPT_DIR}/database-backup-to-oss.mjs" "${TARGET_DIR}/scripts/database-backup-to-oss.mjs" "数据库 OSS 备份脚本"
copy_required_file "${SCRIPT_DIR}/ops/production-health-patrol.mjs" "${TARGET_DIR}/scripts/ops/production-health-patrol.mjs" "生产健康巡检脚本"
copy_required_file "${SCRIPT_DIR}/ops/pingora-current-release-audit.mjs" "${TARGET_DIR}/scripts/ops/pingora-current-release-audit.mjs" "Pingora current release 自审脚本"
copy_required_file "${SCRIPT_DIR}/ops/pingora-direct-rehearsal-status.mjs" "${TARGET_DIR}/scripts/ops/pingora-direct-rehearsal-status.mjs" "Pingora 直连切换彩排状态脚本"
copy_required_file "${SCRIPT_DIR}/ops/pingora-cutover-status-snapshot.mjs" "${TARGET_DIR}/scripts/ops/pingora-cutover-status-snapshot.mjs" "Pingora 直连切换状态快照脚本"
copy_required_file "${SCRIPT_DIR}/ops/pingora-cutover-evidence-bundle.mjs" "${TARGET_DIR}/scripts/ops/pingora-cutover-evidence-bundle.mjs" "Pingora 直连切换证据包脚本"
copy_required_file "${SCRIPT_DIR}/ops/pingora-cutover-command-evidence.mjs" "${TARGET_DIR}/scripts/ops/pingora-cutover-command-evidence.mjs" "Pingora 直连切换命令证据脚本"
copy_required_file "${SCRIPT_DIR}/ops/pingora-cutover-evidence-verify.mjs" "${TARGET_DIR}/scripts/ops/pingora-cutover-evidence-verify.mjs" "Pingora 直连切换证据验真脚本"
copy_required_file "${SCRIPT_DIR}/ops/pingora-cutover-evidence-audit.mjs" "${TARGET_DIR}/scripts/ops/pingora-cutover-evidence-audit.mjs" "Pingora 直连切换证据根目录审计脚本"
copy_required_file "${SCRIPT_DIR}/check-production-health-patrol-env.mjs" "${TARGET_DIR}/scripts/check-production-health-patrol-env.mjs" "生产健康巡检 env 复核脚本"
copy_required_file "${SCRIPT_DIR}/check-pingora-release-readiness.mjs" "${TARGET_DIR}/scripts/check-pingora-release-readiness.mjs" "Pingora release readiness 聚合门禁脚本"
copy_required_file "${SCRIPT_DIR}/check-pingora-direct-preflight.mjs" "${TARGET_DIR}/scripts/check-pingora-direct-preflight.mjs" "Pingora 直连预检脚本"
copy_required_file "${SCRIPT_DIR}/check-pingora-direct-live.mjs" "${TARGET_DIR}/scripts/check-pingora-direct-live.mjs" "Pingora 直连 live smoke 脚本"
copy_required_file "${SCRIPT_DIR}/check-pingora-canary-live.mjs" "${TARGET_DIR}/scripts/check-pingora-canary-live.mjs" "Pingora canary live smoke 脚本"
copy_required_file "${SCRIPT_DIR}/check-pingora-canary-access-log-parity.mjs" "${TARGET_DIR}/scripts/check-pingora-canary-access-log-parity.mjs" "Pingora canary access log 对账脚本"
copy_required_dir "${REPO_ROOT}/deploy/systemd" "${TARGET_DIR}/deploy/systemd" "systemd 配置"
copy_required_dir "${REPO_ROOT}/deploy/nginx" "${TARGET_DIR}/deploy/nginx" "Nginx 配置"
copy_required_dir "${REPO_ROOT}/deploy/env" "${TARGET_DIR}/deploy/env" "生产环境示例"
copy_required_dir "${REPO_ROOT}/deploy/pingora" "${TARGET_DIR}/deploy/pingora" "Pingora 配置"
cat >"${TARGET_DIR}/README.md" <<EOF
# Genarrative Production Release
版本:\`${BUILD_NAME}\`
## 内容
- \`web/\`:主站静态资源,\`web/admin/\` 为后台静态资源,\`web/maintenance.html\` 为维护页。
- \`web.tar.gz\` / \`web.tar.gz.sha256\`Web 发布流水线使用的静态资源压缩包与校验文件。
- \`api-server\`:生产 Linux release 可执行文件。
- \`pingora-gateway\`:可选 Pingora 影子网关可执行文件,仅在显式 \`--include-pingora-gateway\` 时包含。
- \`spacetime_module.wasm\`SpacetimeDB 模块 wasm。
- \`migration-bootstrap-secret.txt\`:构建 \`spacetime_module.wasm\` 时注入的迁移引导密钥,仅用于创建首个迁移操作员;请作为敏感文件保存到 Jenkins Secret Text,授权完成后不要长期留在公开归档中。
- \`*.sha256\`:发布产物 checksum,用于部署前校验。
- \`release-manifest.json\`:发布版本、源码 commit 与产物清单。
- \`scripts/\`:维护模式脚本、数据库导入导出脚本、数据库 OSS 备份脚本、生产健康巡检脚本、Pingora release readiness 聚合门禁、直连启用 / 回退、realpath canary 启用 / 关闭、health patrol env 切换 / TLS 证书同步 / 预检 / direct live smoke / canary live smoke / canary access log 对账 / current release 自审 / 直连彩排状态 / 状态快照 / 证据包 / 命令证据 / 证据验真 / 证据根目录审计脚本、迁移授权脚本和 Jenkins inbound agent systemd 安装脚本。
- \`scripts/deploy/production-api-deploy.sh\`API Deploy 执行入口;同目录的 \`maintenance-on.sh\` / \`maintenance-off.sh\` 必须来自同一发布包。
- \`deploy/\`systemd、Nginx 和生产环境变量示例;\`deploy/nginx/genarrative-dev-http.conf\` 仅供无域名开发服初始化使用。
## Pingora 直连证据总审计
正式切换 runbook 的最终证据根目录总审计必须使用 current release 随包脚本,并同时保留两条真实切换脚本身份要求:
- \`--require-command-executable enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh\`
- \`--require-command-executable rollback-apply:pingora-direct-rollback-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh\`
这两项用于证明 enable / rollback apply 命令证据里真实执行的是 \`/opt/genarrative/current\` 下的随包脚本,而不是 Jenkins 工作区、源码 checkout 或旧发布目录中的同名脚本。
## 生产部署口径
本发布包不包含旧一体化 \`start.sh\`、\`stop.sh\` 或 \`web-server.mjs\`。
生产环境由 systemd 托管 \`spacetimedb.service\` 与 \`genarrative-api.service\`,由 Nginx 托管 \`web/\`。
EOF
write_release_manifest
BUILD_COMPLETED=1
echo "[production-release] 完成: ${TARGET_DIR}"