Add production Jenkins release pipelines

This commit is contained in:
2026-05-02 19:14:13 +08:00
parent 879a53bf8d
commit bdc3257003
38 changed files with 3315 additions and 982 deletions

View File

@@ -0,0 +1,413 @@
#!/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
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'
<!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 静态资源 -> ${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"
chmod +x \
"${TARGET_DIR}/scripts/maintenance-on.sh" \
"${TARGET_DIR}/scripts/maintenance-off.sh" \
"${TARGET_DIR}/scripts/maintenance-status.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" <<EOF
# Genarrative Production Release
版本:\`${BUILD_NAME}\`
## 内容
- \`web/\`:主站静态资源,\`web/admin/\` 为后台静态资源,\`web/maintenance.html\` 为维护页。
- \`web.tar.gz\` / \`web.tar.gz.sha256\`Web 发布流水线使用的静态资源压缩包与校验文件。
- \`api-server\`:生产 Linux release 可执行文件。
- \`spacetime_module.wasm\`SpacetimeDB 模块 wasm。
- \`*.sha256\`:发布产物 checksum用于部署前校验。
- \`release-manifest.json\`:发布版本、源码 commit 与产物清单。
- \`scripts/\`:维护模式脚本、数据库导入导出脚本和迁移授权脚本。
- \`deploy/\`systemd、Nginx 和生产环境变量示例。
## 生产部署口径
本发布包不包含旧一体化 \`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}"