Files
Genarrative/scripts/build-production-release.sh
k88936 8abeb333bb
Project CI / Repository checks (push) Successful in 56s
Project CI / Frontend tests (push) Successful in 4m1s
Project CI / Backend tests (push) Successful in 4m26s
Project CI / Native shell tests (push) Successful in 15m12s
统一画布快速编辑白名单与入口行为 (#140)
- 使用白名单控制快速编辑功能的显示

- 同步浮动工具栏、右键菜单、打开入口和提交门禁
- 禁用角色动作序列帧(整体)、音频和单个图标(explain: backend reject)的快速编辑

- 把快速编辑api改为只接受resource Id | asset Id,删除 image_src 参数, 以保证校验

其中包含生产数据迁移。迁移用于清理历史普通图片错误持久化的 assetKind="image",覆盖 asset → project-resource → showcase → canvas 四个作用域,并包含 operator 鉴权、分批 dry-run、批次 SHA-256 绑定 apply、active 结构化画
  > 布迁移摘要同步及最终零残留复核。

---------

Co-authored-by: kdletters <kdletters@qq.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/140
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
2026-08-12 14:46:24 +08:00

624 lines
25 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"));'
}
persist_generated_migration_bootstrap_secret() {
local secret_dir="${SERVER_RS_DIR}/.spacetimedb/build-secrets"
local secret_path="${secret_dir}/${BUILD_NAME}.txt"
mkdir -p "${secret_dir}"
chmod 700 "${secret_dir}" 2>/dev/null || true
(umask 077; printf '%s\n' "${MIGRATION_BOOTSTRAP_SECRET}" >"${secret_path}")
chmod 600 "${secret_path}" 2>/dev/null || true
echo "[production-release] 自动生成的迁移引导密钥仅写入本机受保护文件: ${secret_path}"
}
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
unset GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET_SHA256
echo "[production-release] 未启用迁移引导密钥。"
return
fi
if [[ -n "${GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET_SHA256:-}" ]]; then
if [[ ! "${GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET_SHA256}" =~ ^[0-9a-fA-F]{64}$ ]]; then
echo "[production-release] 迁移引导密钥 SHA-256 必须是 64 位十六进制。" >&2
exit 1
fi
export GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET_SHA256="${GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET_SHA256,,}"
unset GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET
echo "[production-release] 已准备迁移引导密钥摘要: source=sha256-environment"
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)"
persist_generated_migration_bootstrap_secret
fi
if [[ ! "${MIGRATION_BOOTSTRAP_SECRET}" =~ ^[0-9a-fA-F]{64}$ ]]; then
echo "[production-release] 迁移引导密钥必须是 64 位十六进制高熵值。" >&2
exit 1
fi
local digest_line
digest_line="$(printf '%s' "${MIGRATION_BOOTSTRAP_SECRET}" | sha256sum)"
export GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET_SHA256="${digest_line%% *}"
unset GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET
echo "[production-release] 已准备迁移引导密钥: source=${secret_source}, length=${#MIGRATION_BOOTSTRAP_SECRET}"
}
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_MIGRATION_BOOTSTRAP_SECRET_SHA256="${GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET_SHA256:-}" \
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',
});
}
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,
migration_bootstrap_secret_sha256:
process.env.RELEASE_MIGRATION_BOOTSTRAP_SECRET_SHA256 || null,
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_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
if [[ ! -f "${WEB_DIR}/404.html" ]]; then
echo "[production-release] Web 发布包缺少品牌 404 页面: ${WEB_DIR}/404.html" >&2
exit 1
fi
echo "[production-release] 校验默认维护页不包含临时公告"
(
cd "${REPO_ROOT}"
node scripts/check-maintenance-page.mjs --file "${WEB_DIR}/maintenance.html"
)
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"
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-maintain-external-generation-jobs.mjs" "${TARGET_DIR}/scripts/spacetime-maintain-external-generation-jobs.mjs" "外部生成任务维护脚本"
copy_required_file "${SCRIPT_DIR}/spacetime-clean-editor-image-asset-kind.mjs" "${TARGET_DIR}/scripts/spacetime-clean-editor-image-asset-kind.mjs" "普通图片素材类型清理脚本"
copy_required_file "${SCRIPT_DIR}/spacetime-normalize-editor-character-actions.mjs" "${TARGET_DIR}/scripts/spacetime-normalize-editor-character-actions.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。
- 迁移引导密钥不进入发布包;WASM 仅嵌入 SHA-256 摘要,原始 secret 由 Jenkins Secret File 在 Stdb publish 时受保护地交付。
- \`*.sha256\`:发布产物 checksum,用于部署前校验。
- \`release-manifest.json\`:发布版本、源码 commit 与产物清单。
- \`scripts/\`:维护模式脚本、数据库导入导出脚本、外部生成任务 payload 压缩 / 摘要回填脚本、数据库 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}"