Add production Jenkins release pipelines
This commit is contained in:
413
scripts/build-production-release.sh
Normal file
413
scripts/build-production-release.sh
Normal 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}"
|
||||
12
scripts/deploy/maintenance-off.sh
Normal file
12
scripts/deploy/maintenance-off.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
MAINTENANCE_FILE="${GENARRATIVE_MAINTENANCE_FILE:-/var/lib/genarrative/maintenance/enabled}"
|
||||
|
||||
if [[ -f "${MAINTENANCE_FILE}" ]]; then
|
||||
rm -f "${MAINTENANCE_FILE}"
|
||||
echo "[maintenance] 已退出维护模式: ${MAINTENANCE_FILE}"
|
||||
else
|
||||
echo "[maintenance] 当前未处于维护模式: ${MAINTENANCE_FILE}"
|
||||
fi
|
||||
15
scripts/deploy/maintenance-on.sh
Normal file
15
scripts/deploy/maintenance-on.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
MAINTENANCE_FILE="${GENARRATIVE_MAINTENANCE_FILE:-/var/lib/genarrative/maintenance/enabled}"
|
||||
REASON="${*:-manual}"
|
||||
|
||||
mkdir -p "$(dirname "${MAINTENANCE_FILE}")"
|
||||
{
|
||||
printf "enabled_at=%s\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
printf "reason=%s\n" "${REASON}"
|
||||
} >"${MAINTENANCE_FILE}"
|
||||
|
||||
chmod 0644 "${MAINTENANCE_FILE}"
|
||||
echo "[maintenance] 已进入维护模式: ${MAINTENANCE_FILE}"
|
||||
12
scripts/deploy/maintenance-status.sh
Normal file
12
scripts/deploy/maintenance-status.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
MAINTENANCE_FILE="${GENARRATIVE_MAINTENANCE_FILE:-/var/lib/genarrative/maintenance/enabled}"
|
||||
|
||||
if [[ -f "${MAINTENANCE_FILE}" ]]; then
|
||||
echo "[maintenance] enabled"
|
||||
cat "${MAINTENANCE_FILE}"
|
||||
else
|
||||
echo "[maintenance] disabled"
|
||||
fi
|
||||
138
scripts/deploy/production-api-deploy.sh
Normal file
138
scripts/deploy/production-api-deploy.sh
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法:
|
||||
./scripts/deploy/production-api-deploy.sh --source-dir build/<version> [--version <version>] [--release-root /opt/genarrative/releases] [--current-link /opt/genarrative/current] [--service genarrative-api.service] [--health-url http://127.0.0.1:8082/healthz]
|
||||
|
||||
说明:
|
||||
进入维护模式,校验并发布 api-server 单文件,更新 current 链接,重启 systemd 服务并执行 healthz 检查。
|
||||
失败时保留维护模式。
|
||||
EOF
|
||||
}
|
||||
|
||||
require_argument() {
|
||||
local value="$1"
|
||||
local label="$2"
|
||||
|
||||
if [[ -z "${value}" ]]; then
|
||||
echo "[production-api-deploy] 缺少参数: ${label}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SOURCE_DIR=""
|
||||
VERSION=""
|
||||
RELEASE_ROOT="/opt/genarrative/releases"
|
||||
CURRENT_LINK="/opt/genarrative/current"
|
||||
SERVICE_NAME="genarrative-api.service"
|
||||
HEALTH_URL="http://127.0.0.1:8082/healthz"
|
||||
DEPLOY_COMPLETED=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--source-dir)
|
||||
SOURCE_DIR="${2:?缺少 --source-dir 的值}"
|
||||
shift 2
|
||||
;;
|
||||
--version)
|
||||
VERSION="${2:?缺少 --version 的值}"
|
||||
shift 2
|
||||
;;
|
||||
--release-root)
|
||||
RELEASE_ROOT="${2:?缺少 --release-root 的值}"
|
||||
shift 2
|
||||
;;
|
||||
--current-link)
|
||||
CURRENT_LINK="${2:?缺少 --current-link 的值}"
|
||||
shift 2
|
||||
;;
|
||||
--service)
|
||||
SERVICE_NAME="${2:?缺少 --service 的值}"
|
||||
shift 2
|
||||
;;
|
||||
--health-url)
|
||||
HEALTH_URL="${2:?缺少 --health-url 的值}"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "[production-api-deploy] 未知参数: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
require_argument "${SOURCE_DIR}" "--source-dir"
|
||||
|
||||
if [[ ! -d "${SOURCE_DIR}" ]]; then
|
||||
echo "[production-api-deploy] 发布目录不存在: ${SOURCE_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SOURCE_DIR="$(cd "${SOURCE_DIR}" && pwd)"
|
||||
VERSION="${VERSION:-$(basename "${SOURCE_DIR}")}"
|
||||
|
||||
if [[ ! "${VERSION}" =~ ^[0-9A-Za-z._-]+$ ]]; then
|
||||
echo "[production-api-deploy] --version 只能包含数字、字母、点、下划线和短横线: ${VERSION}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "${SOURCE_DIR}/api-server" || ! -f "${SOURCE_DIR}/api-server.sha256" ]]; then
|
||||
echo "[production-api-deploy] 缺少 api-server 或 api-server.sha256: ${SOURCE_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
on_exit() {
|
||||
local exit_code=$?
|
||||
if [[ "${exit_code}" -ne 0 && "${DEPLOY_COMPLETED}" -ne 1 ]]; then
|
||||
echo "[production-api-deploy] 部署失败,保持维护模式。" >&2
|
||||
fi
|
||||
exit "${exit_code}"
|
||||
}
|
||||
|
||||
trap on_exit EXIT
|
||||
|
||||
"${SCRIPT_DIR}/maintenance-on.sh" "api deploy ${VERSION}"
|
||||
|
||||
echo "[production-api-deploy] 校验 api-server"
|
||||
(
|
||||
cd "${SOURCE_DIR}"
|
||||
sha256sum -c api-server.sha256
|
||||
)
|
||||
|
||||
RELEASE_DIR="${RELEASE_ROOT}/${VERSION}"
|
||||
mkdir -p "${RELEASE_DIR}"
|
||||
cp "${SOURCE_DIR}/api-server" "${RELEASE_DIR}/api-server"
|
||||
chmod +x "${RELEASE_DIR}/api-server"
|
||||
|
||||
if [[ -f "${SOURCE_DIR}/release-manifest.json" ]]; then
|
||||
cp "${SOURCE_DIR}/release-manifest.json" "${RELEASE_DIR}/release-manifest.api-server.json"
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "${CURRENT_LINK}")"
|
||||
ln -sfn "${RELEASE_DIR}" "${CURRENT_LINK}"
|
||||
|
||||
echo "[production-api-deploy] 重启服务: ${SERVICE_NAME}"
|
||||
systemctl restart "${SERVICE_NAME}"
|
||||
|
||||
echo "[production-api-deploy] 等待 healthz: ${HEALTH_URL}"
|
||||
for _ in {1..30}; do
|
||||
if curl -fsS "${HEALTH_URL}" >/dev/null; then
|
||||
"${SCRIPT_DIR}/maintenance-off.sh"
|
||||
DEPLOY_COMPLETED=1
|
||||
echo "[production-api-deploy] 完成: ${RELEASE_DIR}/api-server"
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "[production-api-deploy] healthz 检查超时: ${HEALTH_URL}" >&2
|
||||
exit 1
|
||||
123
scripts/deploy/production-stdb-publish.sh
Normal file
123
scripts/deploy/production-stdb-publish.sh
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法:
|
||||
./scripts/deploy/production-stdb-publish.sh --source-dir build/<version> --database <database> [--server local] [--clear-database]
|
||||
|
||||
说明:
|
||||
进入维护模式,校验 spacetime_module.wasm.sha256,并在生产实例本机执行 spacetime publish。
|
||||
失败时保留维护模式。
|
||||
EOF
|
||||
}
|
||||
|
||||
require_argument() {
|
||||
local value="$1"
|
||||
local label="$2"
|
||||
|
||||
if [[ -z "${value}" ]]; then
|
||||
echo "[production-stdb-publish] 缺少参数: ${label}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
validate_spacetime_database_name() {
|
||||
local database="$1"
|
||||
|
||||
if [[ ! "${database}" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
|
||||
echo "[production-stdb-publish] --database 必须匹配 ^[a-z0-9]+(-[a-z0-9]+)*$: ${database}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SOURCE_DIR=""
|
||||
DATABASE=""
|
||||
SERVER_ALIAS="local"
|
||||
CLEAR_DATABASE=0
|
||||
DEPLOY_COMPLETED=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--source-dir)
|
||||
SOURCE_DIR="${2:?缺少 --source-dir 的值}"
|
||||
shift 2
|
||||
;;
|
||||
--database)
|
||||
DATABASE="${2:?缺少 --database 的值}"
|
||||
shift 2
|
||||
;;
|
||||
--server)
|
||||
SERVER_ALIAS="${2:?缺少 --server 的值}"
|
||||
shift 2
|
||||
;;
|
||||
--clear-database)
|
||||
CLEAR_DATABASE=1
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "[production-stdb-publish] 未知参数: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
require_argument "${SOURCE_DIR}" "--source-dir"
|
||||
require_argument "${DATABASE}" "--database"
|
||||
validate_spacetime_database_name "${DATABASE}"
|
||||
|
||||
if [[ ! -d "${SOURCE_DIR}" ]]; then
|
||||
echo "[production-stdb-publish] 发布目录不存在: ${SOURCE_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SOURCE_DIR="$(cd "${SOURCE_DIR}" && pwd)"
|
||||
|
||||
if [[ ! -f "${SOURCE_DIR}/spacetime_module.wasm" || ! -f "${SOURCE_DIR}/spacetime_module.wasm.sha256" ]]; then
|
||||
echo "[production-stdb-publish] 缺少 spacetime_module.wasm 或 spacetime_module.wasm.sha256: ${SOURCE_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
on_exit() {
|
||||
local exit_code=$?
|
||||
if [[ "${exit_code}" -ne 0 && "${DEPLOY_COMPLETED}" -ne 1 ]]; then
|
||||
echo "[production-stdb-publish] 发布失败,保持维护模式。" >&2
|
||||
fi
|
||||
exit "${exit_code}"
|
||||
}
|
||||
|
||||
trap on_exit EXIT
|
||||
|
||||
"${SCRIPT_DIR}/maintenance-on.sh" "spacetime module publish ${DATABASE}"
|
||||
|
||||
echo "[production-stdb-publish] 校验 wasm"
|
||||
(
|
||||
cd "${SOURCE_DIR}"
|
||||
sha256sum -c spacetime_module.wasm.sha256
|
||||
)
|
||||
|
||||
PUBLISH_ARGS=(
|
||||
publish
|
||||
"${DATABASE}"
|
||||
--server "${SERVER_ALIAS}"
|
||||
--bin-path "${SOURCE_DIR}/spacetime_module.wasm"
|
||||
--yes
|
||||
)
|
||||
|
||||
if [[ "${CLEAR_DATABASE}" -eq 1 ]]; then
|
||||
PUBLISH_ARGS+=(--clear-database)
|
||||
fi
|
||||
|
||||
echo "[production-stdb-publish] 发布 SpacetimeDB module: ${DATABASE} -> ${SERVER_ALIAS}"
|
||||
spacetime "${PUBLISH_ARGS[@]}"
|
||||
|
||||
"${SCRIPT_DIR}/maintenance-off.sh"
|
||||
DEPLOY_COMPLETED=1
|
||||
echo "[production-stdb-publish] 完成"
|
||||
123
scripts/deploy/production-web-deploy.sh
Normal file
123
scripts/deploy/production-web-deploy.sh
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法:
|
||||
./scripts/deploy/production-web-deploy.sh --source-dir build/<version> [--version <version>] [--release-root /opt/genarrative/releases] [--web-link /srv/genarrative/web] [--current-link /opt/genarrative/current]
|
||||
|
||||
说明:
|
||||
校验 web.tar.gz.sha256 后,把 web.tar.gz 解压到 /opt/genarrative/releases/<version>/web,
|
||||
并更新 /srv/genarrative/web 软链接。如果同版本目录已存在 api-server,则同步更新 /opt/genarrative/current。
|
||||
EOF
|
||||
}
|
||||
|
||||
require_argument() {
|
||||
local value="$1"
|
||||
local label="$2"
|
||||
|
||||
if [[ -z "${value}" ]]; then
|
||||
echo "[production-web-deploy] 缺少参数: ${label}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
SOURCE_DIR=""
|
||||
VERSION=""
|
||||
RELEASE_ROOT="/opt/genarrative/releases"
|
||||
CURRENT_LINK="/opt/genarrative/current"
|
||||
WEB_LINK="/srv/genarrative/web"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--source-dir)
|
||||
SOURCE_DIR="${2:?缺少 --source-dir 的值}"
|
||||
shift 2
|
||||
;;
|
||||
--version)
|
||||
VERSION="${2:?缺少 --version 的值}"
|
||||
shift 2
|
||||
;;
|
||||
--release-root)
|
||||
RELEASE_ROOT="${2:?缺少 --release-root 的值}"
|
||||
shift 2
|
||||
;;
|
||||
--current-link)
|
||||
CURRENT_LINK="${2:?缺少 --current-link 的值}"
|
||||
shift 2
|
||||
;;
|
||||
--web-link)
|
||||
WEB_LINK="${2:?缺少 --web-link 的值}"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "[production-web-deploy] 未知参数: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
require_argument "${SOURCE_DIR}" "--source-dir"
|
||||
|
||||
if [[ ! -d "${SOURCE_DIR}" ]]; then
|
||||
echo "[production-web-deploy] 发布目录不存在: ${SOURCE_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SOURCE_DIR="$(cd "${SOURCE_DIR}" && pwd)"
|
||||
VERSION="${VERSION:-$(basename "${SOURCE_DIR}")}"
|
||||
|
||||
if [[ ! "${VERSION}" =~ ^[0-9A-Za-z._-]+$ ]]; then
|
||||
echo "[production-web-deploy] --version 只能包含数字、字母、点、下划线和短横线: ${VERSION}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "${SOURCE_DIR}/web.tar.gz" || ! -f "${SOURCE_DIR}/web.tar.gz.sha256" ]]; then
|
||||
echo "[production-web-deploy] 缺少 web.tar.gz 或 web.tar.gz.sha256: ${SOURCE_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[production-web-deploy] 校验 Web 压缩包"
|
||||
(
|
||||
cd "${SOURCE_DIR}"
|
||||
sha256sum -c web.tar.gz.sha256
|
||||
)
|
||||
|
||||
RELEASE_DIR="${RELEASE_ROOT}/${VERSION}"
|
||||
WEB_TARGET="${RELEASE_DIR}/web"
|
||||
mkdir -p "${RELEASE_DIR}"
|
||||
rm -rf "${WEB_TARGET}"
|
||||
|
||||
echo "[production-web-deploy] 解压 Web 到: ${WEB_TARGET}"
|
||||
tar -xzf "${SOURCE_DIR}/web.tar.gz" -C "${RELEASE_DIR}"
|
||||
test -d "${WEB_TARGET}"
|
||||
|
||||
if [[ -f "${SOURCE_DIR}/release-manifest.json" ]]; then
|
||||
cp "${SOURCE_DIR}/release-manifest.json" "${RELEASE_DIR}/release-manifest.web.json"
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "${CURRENT_LINK}")" "$(dirname "${WEB_LINK}")"
|
||||
ln -sfn "${WEB_TARGET}" "${WEB_LINK}"
|
||||
if [[ -x "${RELEASE_DIR}/api-server" ]]; then
|
||||
ln -sfn "${RELEASE_DIR}" "${CURRENT_LINK}"
|
||||
else
|
||||
echo "[production-web-deploy] ${RELEASE_DIR}/api-server 不存在,仅更新 Web 软链接,保持 current 不变。"
|
||||
fi
|
||||
|
||||
if command -v nginx >/dev/null 2>&1; then
|
||||
echo "[production-web-deploy] nginx -t"
|
||||
nginx -t
|
||||
fi
|
||||
|
||||
if [[ ! -f "${WEB_TARGET}/index.html" ]]; then
|
||||
echo "[production-web-deploy] Web smoke test 失败,缺少 index.html: ${WEB_TARGET}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[production-web-deploy] 完成: ${WEB_TARGET}"
|
||||
54
scripts/jenkins-checkout-source.sh
Normal file
54
scripts/jenkins-checkout-source.sh
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH:-master}"
|
||||
COMMIT_HASH="${COMMIT_HASH:-}"
|
||||
GIT_REMOTE_URL="${GIT_REMOTE_URL:-}"
|
||||
SOURCE_COMMIT_FILE="${SOURCE_COMMIT_FILE:-.jenkins-source-commit}"
|
||||
|
||||
if [[ ! "${SOURCE_BRANCH}" =~ ^[0-9A-Za-z._/-]+$ ]]; then
|
||||
echo "[jenkins-checkout-source] SOURCE_BRANCH 只能包含数字、字母、点、下划线、短横线和斜杠: ${SOURCE_BRANCH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${SOURCE_BRANCH}" == /* || "${SOURCE_BRANCH}" == */ || "${SOURCE_BRANCH}" == *..* ]]; then
|
||||
echo "[jenkins-checkout-source] SOURCE_BRANCH 不能以斜杠开头/结尾,也不能包含连续点号: ${SOURCE_BRANCH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${COMMIT_HASH}" && ! "${COMMIT_HASH}" =~ ^[0-9a-fA-F]{7,40}$ ]]; then
|
||||
echo "[jenkins-checkout-source] COMMIT_HASH 只能填写 7 到 40 位十六进制 Git commit hash: ${COMMIT_HASH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${GIT_REMOTE_URL}" ]]; then
|
||||
git remote set-url origin "${GIT_REMOTE_URL}"
|
||||
fi
|
||||
|
||||
git reset --hard HEAD
|
||||
git fetch --tags --prune origin "+refs/heads/${SOURCE_BRANCH}:refs/remotes/origin/${SOURCE_BRANCH}"
|
||||
|
||||
if [[ "$(git rev-parse --is-shallow-repository 2>/dev/null || echo false)" == "true" ]]; then
|
||||
git fetch --unshallow --tags || true
|
||||
fi
|
||||
|
||||
git cat-file -e "refs/remotes/origin/${SOURCE_BRANCH}^{commit}"
|
||||
|
||||
if [[ -n "${COMMIT_HASH}" ]]; then
|
||||
git cat-file -e "${COMMIT_HASH}^{commit}"
|
||||
RESOLVED_COMMIT="$(git rev-parse "${COMMIT_HASH}^{commit}")"
|
||||
if ! git merge-base --is-ancestor "${RESOLVED_COMMIT}" "refs/remotes/origin/${SOURCE_BRANCH}"; then
|
||||
echo "[jenkins-checkout-source] 指定 commit 不属于 origin/${SOURCE_BRANCH}: ${RESOLVED_COMMIT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
RESOLVED_COMMIT="$(git rev-parse "refs/remotes/origin/${SOURCE_BRANCH}^{commit}")"
|
||||
fi
|
||||
|
||||
git checkout --detach "${RESOLVED_COMMIT}"
|
||||
git reset --hard HEAD
|
||||
git clean -fd
|
||||
|
||||
printf "%s\n" "${RESOLVED_COMMIT}" >"${SOURCE_COMMIT_FILE}"
|
||||
echo "[jenkins-checkout-source] 使用源码: branch=${SOURCE_BRANCH} commit=${RESOLVED_COMMIT}"
|
||||
Reference in New Issue
Block a user