#!/usr/bin/env bash set -euo pipefail usage() { cat <<'EOF' 用法: npm run build:production-release -- --name 说明: 1. 生成生产发布包 build//。 2. 发布包只包含 systemd + Nginx 生产部署需要的文件,不再生成旧 start.sh / stop.sh / web-server.mjs。 3. 默认会构建主站、后台、api-server 与 SpacetimeDB wasm;调试时可用 skip 参数复用已存在产物。 参数: --name 指定 build 子目录名,默认使用当前时间 YYYYmmdd-HHMMSS --component 构建组件: all, web, api-server, spacetime-module;默认 all --skip-web-build 跳过主站与后台构建,仅复制已有 dist 产物 --skip-api-build 跳过 api-server 构建,仅复制已有 release 二进制 --skip-spacetime-build 跳过 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 } 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}" } 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}" \ 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', }); } 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 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 ;; *) 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" 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 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 [[ "${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}" 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' 服务维护中

服务维护中

我们正在更新系统,稍后请重新访问。

MAINTENANCE_HTML fi 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 [[ "${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}/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/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/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_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" "生产环境示例" cat >"${TARGET_DIR}/README.md" <