Add production Jenkins release pipelines
This commit is contained in:
@@ -1,202 +0,0 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
}
|
||||
|
||||
environment {
|
||||
GENARRATIVE_TOOLS_PATH = "/var/lib/jenkins/.nvm/versions/node/v22.22.2/bin:/var/lib/jenkins/.cargo/bin:/var/lib/jenkins/.local/bin:/var/lib/jenkins/bin"
|
||||
PATH = "${GENARRATIVE_TOOLS_PATH}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
}
|
||||
|
||||
parameters {
|
||||
string(name: 'AGENT_LABEL', defaultValue: 'built-in', description: '构建节点标签')
|
||||
string(name: 'GENARRATIVE_WORKSPACE_ROOT', defaultValue: '', description: '源码根目录,留空则使用当前 Jenkins 工作区')
|
||||
string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,指定要构建的 Git commit hash;留空则使用 SCM 当前检出的提交')
|
||||
string(name: 'DATABASE', defaultValue: 'genarrative-pipeline-local-test', description: '发布包默认 SpacetimeDB database')
|
||||
string(name: 'API_PORT', defaultValue: '8082', description: '发布包内 api-server 端口')
|
||||
string(name: 'WEB_PORT', defaultValue: '25001', description: '发布包内静态网站端口,默认 25001')
|
||||
string(name: 'SPACETIME_PORT', defaultValue: '3101', description: '发布包内本地 SpacetimeDB 端口')
|
||||
booleanParam(name: 'CLEAR_DATABASE', defaultValue: false, description: '部署时是否清空 SpacetimeDB 数据后再发布 wasm')
|
||||
booleanParam(name: 'MIGRATE_ON_CONFLICT', defaultValue: true, description: '普通发布遇到 SpacetimeDB schema 冲突时自动导出、清库发布并导入回灌')
|
||||
string(name: 'MIGRATION_DIRECTORY', defaultValue: '', description: '自动迁移 JSON 输出目录,留空则使用部署目录内 database-migrations/<database>')
|
||||
password(name: 'MIGRATION_EXPORT_TOKEN', defaultValue: '', description: '可选,旧库已授权迁移操作员 token,仅部署阶段用于 schema 冲突导出')
|
||||
password(name: 'MIGRATION_IMPORT_TOKEN', defaultValue: '', description: '可选,新库已授权迁移操作员 token,仅部署阶段用于 schema 冲突导入')
|
||||
booleanParam(name: 'RUN_NPM_CI', defaultValue: false, description: '构建前是否执行 npm ci')
|
||||
string(name: 'DEPLOY_JOB_NAME', defaultValue: 'Genarrative-Deploy', description: '部署流水线作业名')
|
||||
string(name: 'DEPLOY_DIRECTORY', defaultValue: '/var/lib/jenkins/deploy/Genarrative', description: '固定部署目录')
|
||||
booleanParam(name: 'RUN_DEPLOY_HOOKS_WITH_SUDO', defaultValue: true, description: 'start.sh / stop.sh 是否通过 sudo -n 执行')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('构建发布包') {
|
||||
agent {
|
||||
label "${params.AGENT_LABEL}"
|
||||
}
|
||||
|
||||
steps {
|
||||
script {
|
||||
// 统一在脚本块里计算版本号,避免 declarative environment 对表达式求值不一致。
|
||||
env.EFFECTIVE_BUILD_VERSION = params.BUILD_VERSION?.trim() ? params.BUILD_VERSION.trim() : env.BUILD_NUMBER
|
||||
// 允许 Jenkins Job 直接指定固定源码目录,未指定时回退到当前工作区。
|
||||
env.WORKSPACE_ROOT = params.GENARRATIVE_WORKSPACE_ROOT?.trim() ? params.GENARRATIVE_WORKSPACE_ROOT.trim() : pwd()
|
||||
def commitHash = params.COMMIT_HASH?.trim()
|
||||
if (commitHash && !(commitHash ==~ /^[0-9a-fA-F]{7,40}$/)) {
|
||||
error('COMMIT_HASH 只能填写 7 到 40 位十六进制 Git commit hash,当前值: ' + commitHash)
|
||||
}
|
||||
env.COMMIT_HASH = commitHash ?: ''
|
||||
def database = params.DATABASE?.trim()
|
||||
if (!database) {
|
||||
error('DATABASE 不能为空。')
|
||||
}
|
||||
if (!(database ==~ /^[a-z0-9]+(-[a-z0-9]+)*$/)) {
|
||||
error('DATABASE 必须匹配 SpacetimeDB 数据库名规则 ^[a-z0-9]+(-[a-z0-9]+)*$,只能使用小写字母、数字,并用单个短横线分隔,当前值: ' + database)
|
||||
}
|
||||
env.EFFECTIVE_DATABASE = database
|
||||
echo "SpacetimeDB 发布数据库: ${env.EFFECTIVE_DATABASE}"
|
||||
def apiPort = params.API_PORT?.trim()
|
||||
if (!apiPort) {
|
||||
error('API_PORT 不能为空。')
|
||||
}
|
||||
if (!(apiPort ==~ /^[0-9]+$/)) {
|
||||
error("API_PORT 必须是数字端口,当前值: ${apiPort}")
|
||||
}
|
||||
if (apiPort.length() > 5) {
|
||||
error("API_PORT 必须在 1-65535 之间,当前值: ${apiPort}")
|
||||
}
|
||||
def parsedApiPort = apiPort.toInteger()
|
||||
if (parsedApiPort < 1 || parsedApiPort > 65535) {
|
||||
error("API_PORT 必须在 1-65535 之间,当前值: ${apiPort}")
|
||||
}
|
||||
env.EFFECTIVE_API_PORT = apiPort
|
||||
def webPort = params.WEB_PORT?.trim()
|
||||
if (!webPort) {
|
||||
error('WEB_PORT 不能为空。')
|
||||
}
|
||||
if (!(webPort ==~ /^[0-9]+$/)) {
|
||||
error("WEB_PORT 必须是数字端口,当前值: ${webPort}")
|
||||
}
|
||||
if (webPort.length() > 5) {
|
||||
error("WEB_PORT 必须在 1-65535 之间,当前值: ${webPort}")
|
||||
}
|
||||
def parsedWebPort = webPort.toInteger()
|
||||
if (parsedWebPort < 1 || parsedWebPort > 65535) {
|
||||
error("WEB_PORT 必须在 1-65535 之间,当前值: ${webPort}")
|
||||
}
|
||||
// 后续构建与下游部署都使用校验后的同一端口值,避免参数空格导致上下游不一致。
|
||||
env.EFFECTIVE_WEB_PORT = webPort
|
||||
def spacetimePort = params.SPACETIME_PORT?.trim()
|
||||
if (!spacetimePort) {
|
||||
error('SPACETIME_PORT 不能为空。')
|
||||
}
|
||||
if (!(spacetimePort ==~ /^[0-9]+$/)) {
|
||||
error("SPACETIME_PORT 必须是数字端口,当前值: ${spacetimePort}")
|
||||
}
|
||||
if (spacetimePort.length() > 5) {
|
||||
error("SPACETIME_PORT 必须在 1-65535 之间,当前值: ${spacetimePort}")
|
||||
}
|
||||
def parsedSpacetimePort = spacetimePort.toInteger()
|
||||
if (parsedSpacetimePort < 1 || parsedSpacetimePort > 65535) {
|
||||
error("SPACETIME_PORT 必须在 1-65535 之间,当前值: ${spacetimePort}")
|
||||
}
|
||||
env.EFFECTIVE_SPACETIME_PORT = spacetimePort
|
||||
// 记录当前构建节点名,部署阶段必须回到同一节点读取本地 build 目录。
|
||||
env.SOURCE_NODE_NAME = env.NODE_NAME
|
||||
}
|
||||
|
||||
dir("${env.WORKSPACE_ROOT}") {
|
||||
checkout scm
|
||||
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
# 每条流水线开头先强制回到 SCM 检出的干净提交,避免固定源码目录残留改动影响本次执行。
|
||||
git reset --hard HEAD
|
||||
requested_commit="${COMMIT_HASH:-}"
|
||||
if [[ -n "${requested_commit}" ]]; then
|
||||
# Jenkins 先按 SCM 配置完成 checkout;如指定 commit,再拉取远端引用并切到该提交构建。
|
||||
git fetch --tags --prune origin "+refs/heads/*:refs/remotes/origin/*" || git fetch --all --tags --prune
|
||||
if [[ "$(git rev-parse --is-shallow-repository 2>/dev/null || echo false)" == "true" ]]; then
|
||||
git fetch --unshallow --tags || true
|
||||
fi
|
||||
git cat-file -e "${requested_commit}^{commit}"
|
||||
resolved_commit="$(git rev-parse "${requested_commit}^{commit}")"
|
||||
git checkout --detach "${resolved_commit}"
|
||||
echo "[build-and-deploy] 使用指定 commit 构建: ${resolved_commit}"
|
||||
else
|
||||
resolved_commit="$(git rev-parse HEAD)"
|
||||
echo "[build-and-deploy] 使用 SCM checkout commit 构建: ${resolved_commit}"
|
||||
fi
|
||||
# 构建前清理工作区内的 Git 变更和未跟踪文件,避免复用固定源码目录时受到上次构建残留影响。
|
||||
# 这里不使用 -x,避免删除 node_modules 等忽略目录后与 RUN_NPM_CI=false 的配置冲突。
|
||||
git reset --hard HEAD
|
||||
git clean -fd
|
||||
echo "${resolved_commit}" > "build-and-deploy-commit.txt"
|
||||
rm -rf "build/${EFFECTIVE_BUILD_VERSION}"
|
||||
'
|
||||
'''
|
||||
|
||||
script {
|
||||
env.EFFECTIVE_COMMIT_HASH = readFile('build-and-deploy-commit.txt').trim()
|
||||
echo "构建 commit: ${env.EFFECTIVE_COMMIT_HASH}"
|
||||
}
|
||||
|
||||
script {
|
||||
// 是否重装依赖交给流水线参数决定,避免每次构建都重复执行 npm ci。
|
||||
if (params.RUN_NPM_CI) {
|
||||
sh 'bash -lc "npm ci"'
|
||||
}
|
||||
}
|
||||
|
||||
sh """
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
# 构建并部署流水线显式透传本地测试参数,避免发布包回退到默认库名或端口。
|
||||
npm run deploy:rust:remote -- --skip-upload \
|
||||
--name "${env.EFFECTIVE_BUILD_VERSION}" \
|
||||
--database "${env.EFFECTIVE_DATABASE}" \
|
||||
--api-port "${env.EFFECTIVE_API_PORT}" \
|
||||
--web-port "${env.EFFECTIVE_WEB_PORT}" \
|
||||
--spacetime-port "${env.EFFECTIVE_SPACETIME_PORT}"
|
||||
test -d "build/${env.EFFECTIVE_BUILD_VERSION}"
|
||||
'
|
||||
"""
|
||||
|
||||
archiveArtifacts artifacts: "build/${env.EFFECTIVE_BUILD_VERSION}/**", fingerprint: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('触发部署') {
|
||||
steps {
|
||||
// 本阶段没有声明 agent,确保触发下游前已经释放构建节点,避免单执行器死锁。
|
||||
build job: params.DEPLOY_JOB_NAME,
|
||||
wait: true,
|
||||
propagate: true,
|
||||
parameters: [
|
||||
string(name: 'SOURCE_NODE_NAME', value: env.SOURCE_NODE_NAME),
|
||||
string(name: 'SOURCE_WORKSPACE_ROOT', value: env.WORKSPACE_ROOT),
|
||||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
|
||||
string(name: 'DEPLOY_DIRECTORY', value: params.DEPLOY_DIRECTORY),
|
||||
string(name: 'WEB_PORT', value: env.EFFECTIVE_WEB_PORT),
|
||||
booleanParam(name: 'CLEAR_DATABASE', value: params.CLEAR_DATABASE),
|
||||
booleanParam(name: 'MIGRATE_ON_CONFLICT', value: params.MIGRATE_ON_CONFLICT),
|
||||
string(name: 'MIGRATION_DIRECTORY', value: params.MIGRATION_DIRECTORY),
|
||||
password(name: 'MIGRATION_EXPORT_TOKEN', value: params.MIGRATION_EXPORT_TOKEN),
|
||||
password(name: 'MIGRATION_IMPORT_TOKEN', value: params.MIGRATION_IMPORT_TOKEN),
|
||||
booleanParam(name: 'RUN_DEPLOY_HOOKS_WITH_SUDO', value: params.RUN_DEPLOY_HOOKS_WITH_SUDO),
|
||||
string(name: 'EXPECTED_UPSTREAM_JOB', value: env.JOB_NAME),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "构建并部署完成,版本号: ${env.EFFECTIVE_BUILD_VERSION}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
}
|
||||
|
||||
environment {
|
||||
GENARRATIVE_TOOLS_PATH = "/var/lib/jenkins/.nvm/versions/node/v22.22.2/bin:/var/lib/jenkins/.cargo/bin:/var/lib/jenkins/.local/bin:/var/lib/jenkins/bin"
|
||||
PATH = "${GENARRATIVE_TOOLS_PATH}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
}
|
||||
|
||||
parameters {
|
||||
string(name: 'AGENT_LABEL', defaultValue: 'built-in', description: '执行节点标签')
|
||||
string(name: 'GENARRATIVE_WORKSPACE_ROOT', defaultValue: '', description: '源码根目录,留空则使用当前 Jenkins 工作区')
|
||||
string(name: 'DATABASE', defaultValue: '', description: 'SpacetimeDB 数据库名,留空则读取环境变量')
|
||||
string(name: 'SERVER', defaultValue: 'maincloud', description: 'SpacetimeDB server 别名,例如 maincloud/local/dev')
|
||||
string(name: 'SERVER_URL', defaultValue: '', description: 'SpacetimeDB server URL,填写后优先于 SERVER')
|
||||
string(name: 'DEPLOY_DIRECTORY', defaultValue: '/var/lib/jenkins/deploy/Genarrative', description: '固定部署目录,ROOT_DIR 为空时使用其 .spacetimedb')
|
||||
string(name: 'ROOT_DIR', defaultValue: '', description: 'spacetime CLI root-dir,可选,优先于 DEPLOY_DIRECTORY')
|
||||
string(name: 'INCLUDE_TABLES', defaultValue: '', description: '可选,逗号分隔的表名白名单')
|
||||
string(name: 'BOOTSTRAP_SECRET', defaultValue: '', description: '可选,授权临时导出 identity 的迁移引导密钥')
|
||||
string(name: 'OUTPUT_DIRECTORY', defaultValue: 'database-exports', description: '导出文件目录,相对源码根目录或绝对路径')
|
||||
string(name: 'EXPORT_NAME', defaultValue: '', description: '导出文件名,留空则自动使用构建号')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('导出数据库') {
|
||||
agent {
|
||||
label "${params.AGENT_LABEL}"
|
||||
}
|
||||
|
||||
steps {
|
||||
script {
|
||||
// 允许 Jenkins Job 指定固定源码目录;未指定时使用当前工作区,方便临时手工执行。
|
||||
env.WORKSPACE_ROOT = params.GENARRATIVE_WORKSPACE_ROOT?.trim() ? params.GENARRATIVE_WORKSPACE_ROOT.trim() : pwd()
|
||||
def deployDirectory = params.DEPLOY_DIRECTORY?.trim()
|
||||
if (!deployDirectory) {
|
||||
error('DEPLOY_DIRECTORY 不能为空。')
|
||||
}
|
||||
env.EFFECTIVE_ROOT_DIR = params.ROOT_DIR?.trim() ? params.ROOT_DIR.trim() : "${deployDirectory}/.spacetimedb"
|
||||
def exportName = params.EXPORT_NAME?.trim()
|
||||
if (!exportName) {
|
||||
exportName = "spacetime-migration-${env.BUILD_NUMBER}.json"
|
||||
}
|
||||
if (!(exportName ==~ /^[A-Za-z0-9._-]+$/)) {
|
||||
error("EXPORT_NAME 只能包含字母、数字、点、下划线和短横线,当前值: ${exportName}")
|
||||
}
|
||||
env.EFFECTIVE_EXPORT_NAME = exportName
|
||||
}
|
||||
|
||||
dir("${env.WORKSPACE_ROOT}") {
|
||||
checkout scm
|
||||
|
||||
sh """
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
# 导出流水线复用固定源码目录时,先清掉本地改动,确保执行的是 Jenkins SCM 检出的脚本。
|
||||
git reset --hard HEAD
|
||||
export_dir="${params.OUTPUT_DIRECTORY}"
|
||||
if [[ -z "\${export_dir}" ]]; then
|
||||
export_dir="database-exports"
|
||||
fi
|
||||
mkdir -p "\${export_dir}"
|
||||
output_path="\${export_dir}/${env.EFFECTIVE_EXPORT_NAME}"
|
||||
args=(scripts/spacetime-export-migration-json.mjs --out "\${output_path}")
|
||||
if [[ -n "${params.DATABASE}" ]]; then
|
||||
args+=(--database "${params.DATABASE}")
|
||||
fi
|
||||
if [[ -n "${params.SERVER}" ]]; then
|
||||
args+=(--server "${params.SERVER}")
|
||||
fi
|
||||
if [[ -n "${params.SERVER_URL}" ]]; then
|
||||
args+=(--server-url "${params.SERVER_URL}")
|
||||
fi
|
||||
if [[ -n "${env.EFFECTIVE_ROOT_DIR}" ]]; then
|
||||
args+=(--root-dir "${env.EFFECTIVE_ROOT_DIR}")
|
||||
fi
|
||||
if [[ -n "${params.INCLUDE_TABLES}" ]]; then
|
||||
args+=(--include "${params.INCLUDE_TABLES}")
|
||||
fi
|
||||
if [[ -n "${params.BOOTSTRAP_SECRET}" ]]; then
|
||||
args+=(--bootstrap-secret "${params.BOOTSTRAP_SECRET}")
|
||||
fi
|
||||
# 复用后端迁移 procedure 导出 JSON,避免 Jenkins 直接拼接表结构和 SQL。
|
||||
node "\${args[@]}"
|
||||
test -s "\${output_path}"
|
||||
'
|
||||
"""
|
||||
|
||||
archiveArtifacts artifacts: "${params.OUTPUT_DIRECTORY ?: 'database-exports'}/${env.EFFECTIVE_EXPORT_NAME}", fingerprint: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "数据库导出完成: ${env.EFFECTIVE_EXPORT_NAME}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
}
|
||||
|
||||
environment {
|
||||
GENARRATIVE_TOOLS_PATH = "/var/lib/jenkins/.nvm/versions/node/v22.22.2/bin:/var/lib/jenkins/.cargo/bin:/var/lib/jenkins/.local/bin:/var/lib/jenkins/bin"
|
||||
PATH = "${GENARRATIVE_TOOLS_PATH}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
}
|
||||
|
||||
parameters {
|
||||
string(name: 'AGENT_LABEL', defaultValue: 'built-in', description: '执行节点标签')
|
||||
string(name: 'GENARRATIVE_WORKSPACE_ROOT', defaultValue: '', description: '源码根目录,留空则使用当前 Jenkins 工作区')
|
||||
string(name: 'DATABASE', defaultValue: '', description: 'SpacetimeDB 数据库名,留空则读取环境变量')
|
||||
string(name: 'SERVER', defaultValue: 'maincloud', description: 'SpacetimeDB server 别名,例如 maincloud/local/dev')
|
||||
string(name: 'SERVER_URL', defaultValue: '', description: 'SpacetimeDB server URL,填写后优先于 SERVER')
|
||||
string(name: 'DEPLOY_DIRECTORY', defaultValue: '/var/lib/jenkins/deploy/Genarrative', description: '固定部署目录,ROOT_DIR 为空时使用其 .spacetimedb')
|
||||
string(name: 'ROOT_DIR', defaultValue: '', description: 'spacetime CLI root-dir,可选,优先于 DEPLOY_DIRECTORY')
|
||||
string(name: 'INPUT_FILE', defaultValue: '', description: '必填,迁移 JSON 文件路径,相对源码根目录或绝对路径')
|
||||
string(name: 'INCLUDE_TABLES', defaultValue: '', description: '可选,逗号分隔的表名白名单')
|
||||
string(name: 'CHUNK_SIZE', defaultValue: '524288', description: '迁移 JSON 分片大小,默认 512KiB,用于规避 SpacetimeDB HTTP 413')
|
||||
booleanParam(name: 'DRY_RUN', defaultValue: true, description: '仅校验导入,不写入数据')
|
||||
booleanParam(name: 'INCREMENTAL', defaultValue: true, description: '增量导入,跳过已存在或冲突的行')
|
||||
booleanParam(name: 'REPLACE_EXISTING', defaultValue: false, description: '覆盖本次文件内涉及的表,不可与 INCREMENTAL 同时启用')
|
||||
string(name: 'BOOTSTRAP_SECRET', defaultValue: '', description: '可选,授权临时导入 identity 的迁移引导密钥')
|
||||
string(name: 'TOKEN', defaultValue: '', description: '可选,SpacetimeDB 客户端连接 token;留空则自动创建临时 identity')
|
||||
string(name: 'NOTE', defaultValue: 'jenkins database import', description: '迁移授权备注')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('校验参数') {
|
||||
agent {
|
||||
label 'built-in'
|
||||
}
|
||||
|
||||
steps {
|
||||
script {
|
||||
if (!params.INPUT_FILE?.trim()) {
|
||||
error('INPUT_FILE 不能为空。')
|
||||
}
|
||||
if (params.INCREMENTAL && params.REPLACE_EXISTING) {
|
||||
error('INCREMENTAL 不能和 REPLACE_EXISTING 同时启用。')
|
||||
}
|
||||
def deployDirectory = params.DEPLOY_DIRECTORY?.trim()
|
||||
if (!deployDirectory) {
|
||||
error('DEPLOY_DIRECTORY 不能为空。')
|
||||
}
|
||||
env.EFFECTIVE_ROOT_DIR = params.ROOT_DIR?.trim() ? params.ROOT_DIR.trim() : "${deployDirectory}/.spacetimedb"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('导入数据库') {
|
||||
agent {
|
||||
label "${params.AGENT_LABEL}"
|
||||
}
|
||||
|
||||
steps {
|
||||
script {
|
||||
// 固定源码目录可复用 Jenkins Agent 上的脚本和依赖,未指定时回退到当前工作区。
|
||||
env.WORKSPACE_ROOT = params.GENARRATIVE_WORKSPACE_ROOT?.trim() ? params.GENARRATIVE_WORKSPACE_ROOT.trim() : pwd()
|
||||
}
|
||||
|
||||
dir("${env.WORKSPACE_ROOT}") {
|
||||
checkout scm
|
||||
|
||||
sh """
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
# 导入流水线复用固定源码目录时,先清掉本地改动,确保迁移脚本来自 Jenkins SCM 检出的版本。
|
||||
git reset --hard HEAD
|
||||
args=(scripts/spacetime-import-migration-json.mjs --in "${params.INPUT_FILE}")
|
||||
if [[ -n "${params.DATABASE}" ]]; then
|
||||
args+=(--database "${params.DATABASE}")
|
||||
fi
|
||||
if [[ -n "${params.SERVER}" ]]; then
|
||||
args+=(--server "${params.SERVER}")
|
||||
fi
|
||||
if [[ -n "${params.SERVER_URL}" ]]; then
|
||||
args+=(--server-url "${params.SERVER_URL}")
|
||||
fi
|
||||
if [[ -n "${env.EFFECTIVE_ROOT_DIR}" ]]; then
|
||||
args+=(--root-dir "${env.EFFECTIVE_ROOT_DIR}")
|
||||
fi
|
||||
if [[ -n "${params.INCLUDE_TABLES}" ]]; then
|
||||
args+=(--include "${params.INCLUDE_TABLES}")
|
||||
fi
|
||||
if [[ -n "${params.CHUNK_SIZE}" ]]; then
|
||||
args+=(--chunk-size "${params.CHUNK_SIZE}")
|
||||
fi
|
||||
if [[ "${params.DRY_RUN}" == "true" ]]; then
|
||||
args+=(--dry-run)
|
||||
fi
|
||||
if [[ "${params.INCREMENTAL}" == "true" ]]; then
|
||||
args+=(--incremental)
|
||||
fi
|
||||
if [[ "${params.REPLACE_EXISTING}" == "true" ]]; then
|
||||
args+=(--replace-existing)
|
||||
fi
|
||||
if [[ -n "${params.BOOTSTRAP_SECRET}" ]]; then
|
||||
args+=(--bootstrap-secret "${params.BOOTSTRAP_SECRET}")
|
||||
fi
|
||||
if [[ -n "${params.TOKEN}" ]]; then
|
||||
args+=(--token "${params.TOKEN}")
|
||||
fi
|
||||
if [[ -n "${params.NOTE}" ]]; then
|
||||
args+=(--note "${params.NOTE}")
|
||||
fi
|
||||
# 导入默认 dry-run,并通过迁移 procedure 写入,保持权限校验和表级统计一致。
|
||||
node "\${args[@]}"
|
||||
'
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "数据库导入流水线完成,dry-run: ${params.DRY_RUN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
}
|
||||
|
||||
environment {
|
||||
GENARRATIVE_TOOLS_PATH = "/var/lib/jenkins/.nvm/versions/node/v22.22.2/bin:/var/lib/jenkins/.cargo/bin:/var/lib/jenkins/.local/bin:/var/lib/jenkins/bin"
|
||||
PATH = "${GENARRATIVE_TOOLS_PATH}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
}
|
||||
|
||||
parameters {
|
||||
string(name: 'SOURCE_NODE_NAME', defaultValue: '', description: '上游构建节点名')
|
||||
string(name: 'SOURCE_WORKSPACE_ROOT', defaultValue: '', description: '上游源码根目录')
|
||||
string(name: 'BUILD_VERSION', defaultValue: '', description: '待部署版本号')
|
||||
string(name: 'DEPLOY_DIRECTORY', defaultValue: '/var/lib/jenkins/deploy/Genarrative', description: '固定部署目录')
|
||||
string(name: 'WEB_PORT', defaultValue: '25001', description: '静态网站监听端口,默认 25001,上游构建并部署流水线会透传同名参数')
|
||||
booleanParam(name: 'CLEAR_DATABASE', defaultValue: false, description: '部署时是否清空 SpacetimeDB 数据后再发布 wasm')
|
||||
booleanParam(name: 'MIGRATE_ON_CONFLICT', defaultValue: true, description: '普通发布遇到 SpacetimeDB schema 冲突时自动导出、清库发布并导入回灌')
|
||||
string(name: 'MIGRATION_DIRECTORY', defaultValue: '', description: '自动迁移 JSON 输出目录,留空则使用部署目录内 database-migrations/<database>')
|
||||
password(name: 'MIGRATION_EXPORT_TOKEN', defaultValue: '', description: '可选,旧库已授权迁移操作员 token,仅用于 schema 冲突导出')
|
||||
password(name: 'MIGRATION_IMPORT_TOKEN', defaultValue: '', description: '可选,新库已授权迁移操作员 token,仅用于 schema 冲突导入')
|
||||
booleanParam(name: 'RUN_DEPLOY_HOOKS_WITH_SUDO', defaultValue: true, description: 'start.sh / stop.sh 是否通过 sudo -n 执行')
|
||||
string(name: 'EXPECTED_UPSTREAM_JOB', defaultValue: '', description: '允许触发本作业的上游作业名')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('校验触发来源') {
|
||||
agent {
|
||||
label 'built-in'
|
||||
}
|
||||
|
||||
steps {
|
||||
script {
|
||||
// 部署流水线允许手动启动;如存在上游触发原因,则继续执行上游作业名门禁。
|
||||
// Pipeline 的 build 步骤通常会把下游触发原因记录成 BuildUpstreamCause,
|
||||
// 直接只查经典 UpstreamCause 会把真实的上游触发误判成“人工执行”。
|
||||
def pipelineUpstreamCauses = currentBuild.getBuildCauses('org.jenkinsci.plugins.workflow.support.steps.build.BuildUpstreamCause')
|
||||
def classicUpstreamCauses = currentBuild.getBuildCauses('hudson.model.Cause$UpstreamCause')
|
||||
def upstreamCause = null
|
||||
|
||||
if (pipelineUpstreamCauses && !pipelineUpstreamCauses.isEmpty()) {
|
||||
upstreamCause = pipelineUpstreamCauses[0]
|
||||
} else if (classicUpstreamCauses && !classicUpstreamCauses.isEmpty()) {
|
||||
upstreamCause = classicUpstreamCauses[0]
|
||||
}
|
||||
|
||||
def actualUpstreamJob = upstreamCause?.upstreamProject ?: ''
|
||||
def expectedUpstreamJob = params.EXPECTED_UPSTREAM_JOB?.trim()
|
||||
def allowedUpstreamJob = env.GENARRATIVE_ALLOWED_UPSTREAM_JOB?.trim()
|
||||
|
||||
if (!params.BUILD_VERSION?.trim()) {
|
||||
error('BUILD_VERSION 不能为空。')
|
||||
}
|
||||
|
||||
if (!params.SOURCE_WORKSPACE_ROOT?.trim()) {
|
||||
error('SOURCE_WORKSPACE_ROOT 不能为空。')
|
||||
}
|
||||
|
||||
if (!params.SOURCE_NODE_NAME?.trim()) {
|
||||
error('SOURCE_NODE_NAME 不能为空。')
|
||||
}
|
||||
|
||||
def webPort = params.WEB_PORT?.trim()
|
||||
if (!webPort) {
|
||||
error('WEB_PORT 不能为空。')
|
||||
}
|
||||
|
||||
if (!(webPort ==~ /^[0-9]+$/)) {
|
||||
error("WEB_PORT 必须是数字端口,当前值: ${webPort}")
|
||||
}
|
||||
|
||||
if (webPort.length() > 5) {
|
||||
error("WEB_PORT 必须在 1-65535 之间,当前值: ${webPort}")
|
||||
}
|
||||
|
||||
def parsedWebPort = webPort.toInteger()
|
||||
if (parsedWebPort < 1 || parsedWebPort > 65535) {
|
||||
error("WEB_PORT 必须在 1-65535 之间,当前值: ${webPort}")
|
||||
}
|
||||
// 部署脚本只接收校验后的端口值,避免手工参数前后空格传到 Bash。
|
||||
env.EFFECTIVE_WEB_PORT = webPort
|
||||
|
||||
if (upstreamCause && !actualUpstreamJob?.trim()) {
|
||||
error('无法从上游触发原因中解析作业名,请检查 Jenkins Pipeline Build Step 插件版本与触发链。')
|
||||
}
|
||||
|
||||
if (actualUpstreamJob && expectedUpstreamJob && actualUpstreamJob != expectedUpstreamJob) {
|
||||
error("上游作业校验失败,期望 ${expectedUpstreamJob},实际 ${actualUpstreamJob}")
|
||||
}
|
||||
|
||||
if (actualUpstreamJob && allowedUpstreamJob && actualUpstreamJob != allowedUpstreamJob) {
|
||||
error("环境门禁校验失败,仅允许 ${allowedUpstreamJob} 触发,实际 ${actualUpstreamJob}")
|
||||
}
|
||||
|
||||
env.UPSTREAM_JOB_NAME = actualUpstreamJob ?: 'MANUAL'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('部署指定版本') {
|
||||
agent {
|
||||
label "${params.SOURCE_NODE_NAME}"
|
||||
}
|
||||
|
||||
steps {
|
||||
script {
|
||||
// 部署脚本使用当前 Deploy 作业 checkout 出来的版本,避免重放旧 build 目录时继续执行旧脚本。
|
||||
env.DEPLOY_SCRIPT_PATH = "${pwd()}/scripts/jenkins-deploy-release.sh"
|
||||
}
|
||||
|
||||
dir("${params.SOURCE_WORKSPACE_ROOT}") {
|
||||
sh """
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
# 部署流水线也先清回上游工作区的当前提交,避免复用目录中的本地改动影响部署脚本或产物选择。
|
||||
git reset --hard HEAD
|
||||
test -d "build/${params.BUILD_VERSION}"
|
||||
deploy_script="${env.DEPLOY_SCRIPT_PATH}"
|
||||
chmod +x "\${deploy_script}"
|
||||
deploy_args=(
|
||||
--source-dir "build/${params.BUILD_VERSION}"
|
||||
--deploy-dir "${params.DEPLOY_DIRECTORY}"
|
||||
--web-port "${env.EFFECTIVE_WEB_PORT}"
|
||||
)
|
||||
if [[ "${params.CLEAR_DATABASE}" == "true" ]]; then
|
||||
deploy_args+=(--clear-database)
|
||||
fi
|
||||
if [[ "${params.MIGRATE_ON_CONFLICT}" == "true" ]]; then
|
||||
deploy_args+=(--migrate-on-conflict)
|
||||
else
|
||||
deploy_args+=(--no-migrate-on-conflict)
|
||||
fi
|
||||
if [[ -n "${params.MIGRATION_DIRECTORY}" ]]; then
|
||||
deploy_args+=(--migration-dir "${params.MIGRATION_DIRECTORY}")
|
||||
fi
|
||||
if [[ -n "${params.MIGRATION_EXPORT_TOKEN}" ]]; then
|
||||
deploy_args+=(--migration-export-token "${params.MIGRATION_EXPORT_TOKEN}")
|
||||
fi
|
||||
if [[ -n "${params.MIGRATION_IMPORT_TOKEN}" ]]; then
|
||||
deploy_args+=(--migration-import-token "${params.MIGRATION_IMPORT_TOKEN}")
|
||||
fi
|
||||
if [[ "${params.RUN_DEPLOY_HOOKS_WITH_SUDO}" == "true" ]]; then
|
||||
deploy_args+=(--hook-with-sudo)
|
||||
fi
|
||||
# 只部署上游已构建好的版本目录,避免部署阶段再次构建产生漂移。
|
||||
"\${deploy_script}" "\${deploy_args[@]}"
|
||||
'
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "部署完成,版本号: ${params.BUILD_VERSION},上游作业: ${env.UPSTREAM_JOB_NAME}"
|
||||
}
|
||||
}
|
||||
}
|
||||
105
jenkins/Jenkinsfile.production-api-build
Normal file
105
jenkins/Jenkinsfile.production-api-build
Normal file
@@ -0,0 +1,105 @@
|
||||
pipeline {
|
||||
agent {
|
||||
label 'linux && genarrative-build'
|
||||
}
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
|
||||
CARGO_HOME = '/var/cache/genarrative-build/cargo-home'
|
||||
CARGO_TARGET_DIR = '/var/cache/genarrative-build/cargo-target/prod-release'
|
||||
CARGO_INCREMENTAL = '0'
|
||||
RUSTC_WRAPPER = 'sccache'
|
||||
SCCACHE_DIR = '/var/cache/genarrative-build/sccache'
|
||||
SCCACHE_CACHE_SIZE = '30G'
|
||||
}
|
||||
|
||||
parameters {
|
||||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '源码分支,默认 master 最新提交')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,指定属于 SOURCE_BRANCH 的 Git commit')
|
||||
string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER')
|
||||
booleanParam(name: 'PUBLISH_AFTER_BUILD', defaultValue: false, description: '构建成功后是否触发 API 发布')
|
||||
string(name: 'DEPLOY_JOB_NAME', defaultValue: 'Genarrative-Api-Deploy', description: 'API 发布流水线作业名')
|
||||
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: 'PUBLISH_AFTER_BUILD=true 时的逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent')
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: 'PUBLISH_AFTER_BUILD=true 且目标为 release 时必须确认已有独立 release 部署 agent')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [[$class: 'CleanBeforeCheckout']],
|
||||
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]],
|
||||
])
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/jenkins-checkout-source.sh
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" \
|
||||
COMMIT_HASH="${COMMIT_HASH}" \
|
||||
GIT_REMOTE_URL="${GIT_REMOTE_URL}" \
|
||||
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
|
||||
scripts/jenkins-checkout-source.sh
|
||||
'
|
||||
'''
|
||||
script {
|
||||
env.SOURCE_COMMIT = readFile('.jenkins-source-commit').trim()
|
||||
env.EFFECTIVE_BUILD_VERSION = params.BUILD_VERSION?.trim() ? params.BUILD_VERSION.trim() : env.BUILD_NUMBER
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build Api') {
|
||||
steps {
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
mkdir -p "${CARGO_HOME}" "${CARGO_TARGET_DIR}" "${SCCACHE_DIR}"
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" SOURCE_COMMIT="${SOURCE_COMMIT}" \
|
||||
npm run build:production-release -- --component api-server --name "${EFFECTIVE_BUILD_VERSION}"
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Archive') {
|
||||
steps {
|
||||
archiveArtifacts artifacts: "build/${env.EFFECTIVE_BUILD_VERSION}/api-server,build/${env.EFFECTIVE_BUILD_VERSION}/api-server.sha256,build/${env.EFFECTIVE_BUILD_VERSION}/release-manifest.json", fingerprint: true
|
||||
}
|
||||
}
|
||||
|
||||
stage('Publish') {
|
||||
when {
|
||||
expression { return params.PUBLISH_AFTER_BUILD }
|
||||
}
|
||||
steps {
|
||||
build job: params.DEPLOY_JOB_NAME,
|
||||
wait: true,
|
||||
propagate: true,
|
||||
parameters: [
|
||||
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH),
|
||||
string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT),
|
||||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
|
||||
string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET),
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: params.CONFIRM_RELEASE_DEPLOY_AGENT),
|
||||
string(name: 'BUILD_JOB_NAME', value: env.JOB_NAME),
|
||||
string(name: 'BUILD_NUMBER_TO_DEPLOY', value: env.BUILD_NUMBER),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "API 构建完成: version=${env.EFFECTIVE_BUILD_VERSION}, commit=${env.SOURCE_COMMIT}"
|
||||
}
|
||||
}
|
||||
}
|
||||
119
jenkins/Jenkinsfile.production-api-deploy
Normal file
119
jenkins/Jenkinsfile.production-api-deploy
Normal file
@@ -0,0 +1,119 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
|
||||
}
|
||||
|
||||
parameters {
|
||||
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: '逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent')
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent;当前 Linux 开发/构建/开发部署 agent 不可冒充 release 部署机')
|
||||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '部署脚本来源分支')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '部署脚本来源 commit;上游触发时传实际构建 commit')
|
||||
string(name: 'BUILD_VERSION', defaultValue: '', description: '待发布版本号')
|
||||
string(name: 'BUILD_JOB_NAME', defaultValue: 'Genarrative-Api-Build', description: 'API 构建流水线作业名')
|
||||
string(name: 'BUILD_NUMBER_TO_DEPLOY', defaultValue: '', description: '要复制归档产物的上游构建号')
|
||||
string(name: 'RELEASE_ROOT', defaultValue: '/opt/genarrative/releases', description: '生产 release 根目录')
|
||||
string(name: 'CURRENT_LINK', defaultValue: '/opt/genarrative/current', description: '当前版本软链接')
|
||||
string(name: 'SERVICE_NAME', defaultValue: 'genarrative-api.service', description: 'systemd 服务名')
|
||||
string(name: 'HEALTH_URL', defaultValue: 'http://127.0.0.1:8082/healthz', description: '本机健康检查地址')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Prepare') {
|
||||
agent {
|
||||
label 'linux && genarrative-build'
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
if (params.DEPLOY_TARGET == 'release' && !params.CONFIRM_RELEASE_DEPLOY_AGENT) {
|
||||
error('release 部署需要先配置独立 release 部署 agent,并勾选 CONFIRM_RELEASE_DEPLOY_AGENT。当前 Linux 开发/构建/开发部署 agent 不能执行 release 部署。')
|
||||
}
|
||||
if (!params.BUILD_VERSION?.trim()) {
|
||||
error('BUILD_VERSION 不能为空。')
|
||||
}
|
||||
if (!params.BUILD_JOB_NAME?.trim()) {
|
||||
error('BUILD_JOB_NAME 不能为空。')
|
||||
}
|
||||
if (!params.BUILD_NUMBER_TO_DEPLOY?.trim()) {
|
||||
error('BUILD_NUMBER_TO_DEPLOY 不能为空。')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Checkout Deploy Scripts') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [[$class: 'CleanBeforeCheckout']],
|
||||
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]],
|
||||
])
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/jenkins-checkout-source.sh
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" \
|
||||
COMMIT_HASH="${COMMIT_HASH}" \
|
||||
GIT_REMOTE_URL="${GIT_REMOTE_URL}" \
|
||||
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
|
||||
scripts/jenkins-checkout-source.sh
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Fetch Artifact') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
copyArtifacts(
|
||||
projectName: params.BUILD_JOB_NAME,
|
||||
selector: specific(params.BUILD_NUMBER_TO_DEPLOY),
|
||||
filter: "build/${params.BUILD_VERSION}/api-server,build/${params.BUILD_VERSION}/api-server.sha256,build/${params.BUILD_VERSION}/release-manifest.json",
|
||||
target: '.',
|
||||
fingerprintArtifacts: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy Api') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/deploy/production-api-deploy.sh scripts/deploy/maintenance-on.sh scripts/deploy/maintenance-off.sh
|
||||
scripts/deploy/production-api-deploy.sh \
|
||||
--source-dir "build/${BUILD_VERSION}" \
|
||||
--version "${BUILD_VERSION}" \
|
||||
--release-root "${RELEASE_ROOT}" \
|
||||
--current-link "${CURRENT_LINK}" \
|
||||
--service "${SERVICE_NAME}" \
|
||||
--health-url "${HEALTH_URL}"
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "API 发布完成: version=${params.BUILD_VERSION}"
|
||||
}
|
||||
}
|
||||
}
|
||||
198
jenkins/Jenkinsfile.production-database-export
Normal file
198
jenkins/Jenkinsfile.production-database-export
Normal file
@@ -0,0 +1,198 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
|
||||
}
|
||||
|
||||
parameters {
|
||||
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: '逻辑导出目标;development 使用当前 Linux 开发/构建/开发部署 agent')
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent;当前 Linux 开发/构建/开发部署 agent 不可冒充 release 部署机')
|
||||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '导出脚本来源分支')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '导出脚本来源 commit')
|
||||
string(name: 'DATABASE', defaultValue: 'genarrative-prod', description: 'SpacetimeDB database')
|
||||
string(name: 'SPACETIME_SERVER', defaultValue: 'local', description: 'SpacetimeDB server alias')
|
||||
string(name: 'SPACETIME_SERVER_URL', defaultValue: '', description: '显式 SpacetimeDB server URL,填写后优先于 SPACETIME_SERVER')
|
||||
string(name: 'SPACETIME_ROOT_DIR', defaultValue: '/stdb', description: 'spacetime CLI root-dir;release 自托管默认 /stdb')
|
||||
string(name: 'INCLUDE_TABLES', defaultValue: '', description: '可选,逗号分隔的表名白名单')
|
||||
string(name: 'WORKSPACE_EXPORT_DIRECTORY', defaultValue: 'database-exports', description: 'Jenkins workspace 内的导出目录,用于归档')
|
||||
string(name: 'SERVER_BACKUP_DIRECTORY', defaultValue: '/var/lib/genarrative/database-exports', description: '可选,额外保存在目标机器上的备份目录;留空则不保存服务器副本')
|
||||
string(name: 'EXPORT_NAME', defaultValue: '', description: '导出文件名,留空则使用 spacetime-migration-<BUILD_NUMBER>.json')
|
||||
string(name: 'TOKEN_CREDENTIAL_ID', defaultValue: '', description: '可选,SpacetimeDB 客户端连接 token 的 Jenkins Secret Text 凭据 ID')
|
||||
string(name: 'BOOTSTRAP_SECRET_CREDENTIAL_ID', defaultValue: '', description: '可选,迁移 bootstrap secret 的 Jenkins Secret Text 凭据 ID')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Prepare') {
|
||||
agent {
|
||||
label 'linux && genarrative-build'
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
if (params.DEPLOY_TARGET == 'release' && !params.CONFIRM_RELEASE_DEPLOY_AGENT) {
|
||||
error('release 数据库导出需要先配置独立 release 部署 agent,并勾选 CONFIRM_RELEASE_DEPLOY_AGENT。')
|
||||
}
|
||||
if (!params.DATABASE?.trim()) {
|
||||
error('DATABASE 不能为空。')
|
||||
}
|
||||
if (!(params.DATABASE.trim() ==~ /^[a-z0-9]+(-[a-z0-9]+)*$/)) {
|
||||
error("DATABASE 必须匹配 ^[a-z0-9]+(-[a-z0-9]+)*$: ${params.DATABASE}")
|
||||
}
|
||||
|
||||
def spacetimeRootDir = params.SPACETIME_ROOT_DIR?.trim() ? params.SPACETIME_ROOT_DIR.trim() : '/stdb'
|
||||
if (!spacetimeRootDir.startsWith('/') || spacetimeRootDir.contains('..')) {
|
||||
error("SPACETIME_ROOT_DIR 必须是 Linux 绝对路径且不能包含 ..: ${spacetimeRootDir}")
|
||||
}
|
||||
|
||||
def serverBackupDirectory = params.SERVER_BACKUP_DIRECTORY?.trim()
|
||||
if (serverBackupDirectory && (!serverBackupDirectory.startsWith('/') || serverBackupDirectory.contains('..'))) {
|
||||
error("SERVER_BACKUP_DIRECTORY 必须是 Linux 绝对路径且不能包含 ..: ${serverBackupDirectory}")
|
||||
}
|
||||
|
||||
def exportDirectory = params.WORKSPACE_EXPORT_DIRECTORY?.trim() ? params.WORKSPACE_EXPORT_DIRECTORY.trim() : 'database-exports'
|
||||
if (exportDirectory.startsWith('/') || exportDirectory.contains('..') || !(exportDirectory ==~ /^[A-Za-z0-9._\/-]+$/)) {
|
||||
error("WORKSPACE_EXPORT_DIRECTORY 必须是安全的相对路径: ${exportDirectory}")
|
||||
}
|
||||
|
||||
def exportName = params.EXPORT_NAME?.trim()
|
||||
if (!exportName) {
|
||||
exportName = "spacetime-migration-${env.BUILD_NUMBER}.json"
|
||||
}
|
||||
if (!(exportName ==~ /^[A-Za-z0-9._-]+$/)) {
|
||||
error("EXPORT_NAME 只能包含字母、数字、点、下划线和短横线: ${exportName}")
|
||||
}
|
||||
|
||||
env.WORKSPACE_EXPORT_DIRECTORY = exportDirectory
|
||||
env.EFFECTIVE_EXPORT_NAME = exportName
|
||||
env.EFFECTIVE_SPACETIME_ROOT_DIR = spacetimeRootDir
|
||||
env.EFFECTIVE_SERVER_BACKUP_DIRECTORY = serverBackupDirectory ?: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Export Database') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [[$class: 'CleanBeforeCheckout']],
|
||||
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]],
|
||||
])
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/jenkins-checkout-source.sh
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" \
|
||||
COMMIT_HASH="${COMMIT_HASH}" \
|
||||
GIT_REMOTE_URL="${GIT_REMOTE_URL}" \
|
||||
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
|
||||
scripts/jenkins-checkout-source.sh
|
||||
'
|
||||
'''
|
||||
script {
|
||||
def credentialBindings = []
|
||||
if (params.TOKEN_CREDENTIAL_ID?.trim()) {
|
||||
credentialBindings.add(string(credentialsId: params.TOKEN_CREDENTIAL_ID.trim(), variable: 'GENARRATIVE_SPACETIME_TOKEN'))
|
||||
}
|
||||
if (params.BOOTSTRAP_SECRET_CREDENTIAL_ID?.trim()) {
|
||||
credentialBindings.add(string(credentialsId: params.BOOTSTRAP_SECRET_CREDENTIAL_ID.trim(), variable: 'GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET'))
|
||||
}
|
||||
|
||||
def exportStep = {
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
|
||||
chmod +x scripts/deploy/maintenance-on.sh scripts/deploy/maintenance-off.sh
|
||||
|
||||
export_dir="${WORKSPACE_EXPORT_DIRECTORY}"
|
||||
output_path="${export_dir}/${EFFECTIVE_EXPORT_NAME}"
|
||||
mkdir -p "${export_dir}"
|
||||
|
||||
maintenance_entered=0
|
||||
on_exit() {
|
||||
local exit_code=$?
|
||||
if [[ "${exit_code}" -ne 0 && "${maintenance_entered}" -eq 1 ]]; then
|
||||
echo "[database-export] 导出失败,保持维护模式。" >&2
|
||||
elif [[ "${exit_code}" -ne 0 ]]; then
|
||||
echo "[database-export] 导出准备失败,尚未进入维护模式。" >&2
|
||||
fi
|
||||
exit "${exit_code}"
|
||||
}
|
||||
trap on_exit EXIT
|
||||
|
||||
scripts/deploy/maintenance-on.sh "database export ${DATABASE}"
|
||||
maintenance_entered=1
|
||||
|
||||
args=(scripts/spacetime-export-migration-json.mjs --out "${output_path}" --database "${DATABASE}")
|
||||
if [[ -n "${SPACETIME_SERVER_URL}" ]]; then
|
||||
args+=(--server-url "${SPACETIME_SERVER_URL}")
|
||||
elif [[ -n "${SPACETIME_SERVER}" ]]; then
|
||||
args+=(--server "${SPACETIME_SERVER}")
|
||||
fi
|
||||
if [[ -n "${EFFECTIVE_SPACETIME_ROOT_DIR}" ]]; then
|
||||
args+=(--root-dir "${EFFECTIVE_SPACETIME_ROOT_DIR}")
|
||||
fi
|
||||
if [[ -n "${INCLUDE_TABLES}" ]]; then
|
||||
args+=(--include "${INCLUDE_TABLES}")
|
||||
fi
|
||||
args+=(--note "jenkins database export ${BUILD_TAG}")
|
||||
|
||||
node "${args[@]}"
|
||||
test -s "${output_path}"
|
||||
sha256sum "${output_path}" >"${output_path}.sha256"
|
||||
|
||||
if [[ -n "${EFFECTIVE_SERVER_BACKUP_DIRECTORY}" ]]; then
|
||||
mkdir -p "${EFFECTIVE_SERVER_BACKUP_DIRECTORY}"
|
||||
install -m 0640 "${output_path}" "${EFFECTIVE_SERVER_BACKUP_DIRECTORY}/${EFFECTIVE_EXPORT_NAME}"
|
||||
install -m 0640 "${output_path}.sha256" "${EFFECTIVE_SERVER_BACKUP_DIRECTORY}/${EFFECTIVE_EXPORT_NAME}.sha256"
|
||||
fi
|
||||
|
||||
echo "[database-export] 完成: ${output_path}, source_commit=$(cat .jenkins-source-commit)"
|
||||
'
|
||||
'''
|
||||
}
|
||||
|
||||
boolean archiveSucceeded = false
|
||||
try {
|
||||
if (credentialBindings) {
|
||||
withCredentials(credentialBindings) {
|
||||
exportStep()
|
||||
}
|
||||
} else {
|
||||
exportStep()
|
||||
}
|
||||
archiveArtifacts artifacts: "${env.WORKSPACE_EXPORT_DIRECTORY}/${env.EFFECTIVE_EXPORT_NAME},${env.WORKSPACE_EXPORT_DIRECTORY}/${env.EFFECTIVE_EXPORT_NAME}.sha256", fingerprint: true
|
||||
archiveSucceeded = true
|
||||
} finally {
|
||||
if (archiveSucceeded) {
|
||||
// 先确认导出和归档都已完成,再退出维护模式,避免归档异常把站点留在维护页。
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
scripts/deploy/maintenance-off.sh
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "数据库导出完成: target=${params.DEPLOY_TARGET}, database=${params.DATABASE}, file=${env.EFFECTIVE_EXPORT_NAME}"
|
||||
}
|
||||
}
|
||||
}
|
||||
315
jenkins/Jenkinsfile.production-database-import
Normal file
315
jenkins/Jenkinsfile.production-database-import
Normal file
@@ -0,0 +1,315 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
|
||||
}
|
||||
|
||||
parameters {
|
||||
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: '逻辑导入目标;development 使用当前 Linux 开发/构建/开发部署 agent')
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent;当前 Linux 开发/构建/开发部署 agent 不可冒充 release 部署机')
|
||||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '导入脚本来源分支')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '导入脚本来源 commit')
|
||||
string(name: 'DATABASE', defaultValue: 'genarrative-prod', description: 'SpacetimeDB database')
|
||||
string(name: 'SPACETIME_SERVER', defaultValue: 'local', description: 'SpacetimeDB server alias')
|
||||
string(name: 'SPACETIME_SERVER_URL', defaultValue: '', description: '显式 SpacetimeDB server URL,填写后优先于 SPACETIME_SERVER')
|
||||
string(name: 'SPACETIME_ROOT_DIR', defaultValue: '/stdb', description: 'spacetime CLI root-dir;release 自托管默认 /stdb')
|
||||
choice(name: 'INPUT_SOURCE', choices: ['pipeline_archive', 'manual_upload'], description: '导入数据源;pipeline_archive 从导出流水线归档获取,manual_upload 使用本次构建手动上传文件')
|
||||
string(name: 'INPUT_FILE', defaultValue: '', description: 'pipeline_archive 模式可选;留空时使用导出流水线默认归档路径 database-exports/spacetime-migration-<导出构建号>.json')
|
||||
string(name: 'EXPORT_JOB_NAME', defaultValue: 'Genarrative-Database-Export', description: 'pipeline_archive 模式使用的数据库导出流水线作业名')
|
||||
string(name: 'EXPORT_BUILD_NUMBER_TO_IMPORT', defaultValue: '', description: 'pipeline_archive 模式必填,要复制 INPUT_FILE 的导出构建号')
|
||||
stashedFile 'MANUAL_INPUT_FILE'
|
||||
string(name: 'INCLUDE_TABLES', defaultValue: '', description: '可选,逗号分隔的表名白名单')
|
||||
string(name: 'CHUNK_SIZE', defaultValue: '524288', description: '迁移 JSON 分片大小,默认 512KiB,用于规避 HTTP 413')
|
||||
booleanParam(name: 'DRY_RUN', defaultValue: true, description: '只校验导入,不写入数据')
|
||||
booleanParam(name: 'INCREMENTAL', defaultValue: true, description: '增量导入,跳过已存在或冲突的行')
|
||||
booleanParam(name: 'REPLACE_EXISTING', defaultValue: false, description: '覆盖本次文件内涉及的表,不可与 INCREMENTAL 同时启用')
|
||||
booleanParam(name: 'CONFIRM_IMPORT', defaultValue: false, description: 'DRY_RUN=false 时必须勾选')
|
||||
string(name: 'CONFIRM_DATABASE', defaultValue: '', description: 'DRY_RUN=false 时必须填写与 DATABASE 完全一致')
|
||||
string(name: 'CONFIRM_INPUT_FILE', defaultValue: '', description: 'DRY_RUN=false 时必须确认输入文件;pipeline_archive 填实际归档输入路径,manual_upload 填上传原始文件名')
|
||||
booleanParam(name: 'CONFIRM_REPLACE_EXISTING', defaultValue: false, description: 'REPLACE_EXISTING=true 且 DRY_RUN=false 时必须勾选')
|
||||
string(name: 'PRE_IMPORT_BACKUP_DIRECTORY', defaultValue: 'database-pre-import-backups', description: 'Jenkins workspace 内的导入前备份目录,用于归档')
|
||||
string(name: 'SERVER_BACKUP_DIRECTORY', defaultValue: '/var/lib/genarrative/database-backups', description: '可选,额外保存在目标机器上的导入前备份目录;留空则不保存服务器副本')
|
||||
booleanParam(name: 'RUN_SMOKE_TEST', defaultValue: true, description: '导入成功后是否执行服务健康检查')
|
||||
string(name: 'SMOKE_HEALTH_URL', defaultValue: 'http://127.0.0.1:8082/healthz', description: '目标机器本机健康检查地址')
|
||||
string(name: 'TOKEN_CREDENTIAL_ID', defaultValue: '', description: '可选,SpacetimeDB 客户端连接 token 的 Jenkins Secret Text 凭据 ID')
|
||||
string(name: 'BOOTSTRAP_SECRET_CREDENTIAL_ID', defaultValue: '', description: '可选,迁移 bootstrap secret 的 Jenkins Secret Text 凭据 ID')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Prepare') {
|
||||
agent {
|
||||
label 'linux && genarrative-build'
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
if (params.DEPLOY_TARGET == 'release' && !params.CONFIRM_RELEASE_DEPLOY_AGENT) {
|
||||
error('release 数据库导入需要先配置独立 release 部署 agent,并勾选 CONFIRM_RELEASE_DEPLOY_AGENT。')
|
||||
}
|
||||
if (!params.DATABASE?.trim()) {
|
||||
error('DATABASE 不能为空。')
|
||||
}
|
||||
if (!(params.DATABASE.trim() ==~ /^[a-z0-9]+(-[a-z0-9]+)*$/)) {
|
||||
error("DATABASE 必须匹配 ^[a-z0-9]+(-[a-z0-9]+)*$: ${params.DATABASE}")
|
||||
}
|
||||
|
||||
def inputSource = params.INPUT_SOURCE?.trim()
|
||||
if (!(inputSource in ['pipeline_archive', 'manual_upload'])) {
|
||||
error("INPUT_SOURCE 只能是 pipeline_archive 或 manual_upload,当前值: ${params.INPUT_SOURCE}")
|
||||
}
|
||||
def manualInputFilename = env.MANUAL_INPUT_FILE_FILENAME?.trim()
|
||||
if (inputSource == 'pipeline_archive') {
|
||||
if (!params.EXPORT_JOB_NAME?.trim()) {
|
||||
error('INPUT_SOURCE=pipeline_archive 时 EXPORT_JOB_NAME 不能为空。')
|
||||
}
|
||||
if (!params.EXPORT_BUILD_NUMBER_TO_IMPORT?.trim()) {
|
||||
error('INPUT_SOURCE=pipeline_archive 时 EXPORT_BUILD_NUMBER_TO_IMPORT 不能为空。')
|
||||
}
|
||||
if (!(params.EXPORT_BUILD_NUMBER_TO_IMPORT.trim() ==~ /^[1-9][0-9]*$/)) {
|
||||
error("INPUT_SOURCE=pipeline_archive 时 EXPORT_BUILD_NUMBER_TO_IMPORT 必须是导出流水线构建号: ${params.EXPORT_BUILD_NUMBER_TO_IMPORT}")
|
||||
}
|
||||
def pipelineInputFile = params.INPUT_FILE?.trim()
|
||||
if (!pipelineInputFile) {
|
||||
pipelineInputFile = "database-exports/spacetime-migration-${params.EXPORT_BUILD_NUMBER_TO_IMPORT.trim()}.json"
|
||||
}
|
||||
if (pipelineInputFile.startsWith('/')) {
|
||||
error('INPUT_SOURCE=pipeline_archive 时 INPUT_FILE 必须是 Jenkins 归档内的 workspace 相对路径。')
|
||||
}
|
||||
if (pipelineInputFile.contains('..') || !(pipelineInputFile ==~ /^[A-Za-z0-9._\/-]+$/)) {
|
||||
error("INPUT_SOURCE=pipeline_archive 时 INPUT_FILE 必须是安全的归档相对路径: ${pipelineInputFile}")
|
||||
}
|
||||
if (manualInputFilename) {
|
||||
error('INPUT_SOURCE=pipeline_archive 时不能同时上传 MANUAL_INPUT_FILE。')
|
||||
}
|
||||
env.EFFECTIVE_PIPELINE_ARCHIVE_INPUT_FILE = pipelineInputFile
|
||||
} else {
|
||||
if (!manualInputFilename) {
|
||||
error('INPUT_SOURCE=manual_upload 时必须上传 MANUAL_INPUT_FILE。')
|
||||
}
|
||||
if (params.EXPORT_BUILD_NUMBER_TO_IMPORT?.trim()) {
|
||||
error('INPUT_SOURCE=manual_upload 时不能填写 EXPORT_BUILD_NUMBER_TO_IMPORT。')
|
||||
}
|
||||
if (params.INPUT_FILE?.trim()) {
|
||||
error('INPUT_SOURCE=manual_upload 时不能填写 INPUT_FILE;请使用 MANUAL_INPUT_FILE 上传数据源。')
|
||||
}
|
||||
}
|
||||
if (params.INCREMENTAL && params.REPLACE_EXISTING) {
|
||||
error('INCREMENTAL 不能和 REPLACE_EXISTING 同时启用。')
|
||||
}
|
||||
if (!params.DRY_RUN) {
|
||||
if (!params.CONFIRM_IMPORT) {
|
||||
error('DRY_RUN=false 时必须勾选 CONFIRM_IMPORT。')
|
||||
}
|
||||
if (params.CONFIRM_DATABASE?.trim() != params.DATABASE.trim()) {
|
||||
error('DRY_RUN=false 时 CONFIRM_DATABASE 必须与 DATABASE 完全一致。')
|
||||
}
|
||||
if (inputSource == 'pipeline_archive' && params.CONFIRM_INPUT_FILE?.trim() != env.EFFECTIVE_PIPELINE_ARCHIVE_INPUT_FILE) {
|
||||
error('DRY_RUN=false 时 CONFIRM_INPUT_FILE 必须与实际归档输入路径完全一致。')
|
||||
}
|
||||
if (inputSource == 'manual_upload' && !params.CONFIRM_INPUT_FILE?.trim()) {
|
||||
error('DRY_RUN=false 且 INPUT_SOURCE=manual_upload 时 CONFIRM_INPUT_FILE 必须填写上传文件原始文件名。')
|
||||
}
|
||||
if (inputSource == 'manual_upload' && params.CONFIRM_INPUT_FILE?.trim() != manualInputFilename) {
|
||||
error('DRY_RUN=false 且 INPUT_SOURCE=manual_upload 时 CONFIRM_INPUT_FILE 必须与上传文件原始文件名完全一致。')
|
||||
}
|
||||
if (params.REPLACE_EXISTING && !params.CONFIRM_REPLACE_EXISTING) {
|
||||
error('REPLACE_EXISTING=true 且 DRY_RUN=false 时必须勾选 CONFIRM_REPLACE_EXISTING。')
|
||||
}
|
||||
}
|
||||
|
||||
def backupDirectory = params.PRE_IMPORT_BACKUP_DIRECTORY?.trim() ? params.PRE_IMPORT_BACKUP_DIRECTORY.trim() : 'database-pre-import-backups'
|
||||
if (backupDirectory.startsWith('/') || backupDirectory.contains('..') || !(backupDirectory ==~ /^[A-Za-z0-9._\/-]+$/)) {
|
||||
error("PRE_IMPORT_BACKUP_DIRECTORY 必须是安全的相对路径: ${backupDirectory}")
|
||||
}
|
||||
|
||||
env.PRE_IMPORT_BACKUP_DIRECTORY = backupDirectory
|
||||
env.EFFECTIVE_PRE_IMPORT_BACKUP_NAME = "pre-import-${env.BUILD_NUMBER}.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Import Database') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [[$class: 'CleanBeforeCheckout']],
|
||||
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]],
|
||||
])
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/jenkins-checkout-source.sh
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" \
|
||||
COMMIT_HASH="${COMMIT_HASH}" \
|
||||
GIT_REMOTE_URL="${GIT_REMOTE_URL}" \
|
||||
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
|
||||
scripts/jenkins-checkout-source.sh
|
||||
'
|
||||
'''
|
||||
script {
|
||||
if (params.INPUT_SOURCE == 'pipeline_archive') {
|
||||
echo "[database-import] 使用归档数据源: job=${params.EXPORT_JOB_NAME}, build=${params.EXPORT_BUILD_NUMBER_TO_IMPORT}, file=${env.EFFECTIVE_PIPELINE_ARCHIVE_INPUT_FILE}"
|
||||
copyArtifacts(
|
||||
projectName: params.EXPORT_JOB_NAME,
|
||||
selector: specific(params.EXPORT_BUILD_NUMBER_TO_IMPORT.trim()),
|
||||
filter: "${env.EFFECTIVE_PIPELINE_ARCHIVE_INPUT_FILE},${env.EFFECTIVE_PIPELINE_ARCHIVE_INPUT_FILE}.sha256",
|
||||
target: '.',
|
||||
fingerprintArtifacts: true
|
||||
)
|
||||
env.EFFECTIVE_INPUT_FILE = env.EFFECTIVE_PIPELINE_ARCHIVE_INPUT_FILE
|
||||
} else {
|
||||
echo "[database-import] 使用手动上传数据源: original_filename=${env.MANUAL_INPUT_FILE_FILENAME}"
|
||||
sh 'bash -lc "rm -rf manual-import-upload && mkdir -p manual-import-upload"'
|
||||
dir('manual-import-upload') {
|
||||
unstash 'MANUAL_INPUT_FILE'
|
||||
}
|
||||
env.EFFECTIVE_INPUT_FILE = 'manual-import-upload/MANUAL_INPUT_FILE'
|
||||
if (!params.DRY_RUN) {
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
manual_filename="${MANUAL_INPUT_FILE_FILENAME:-}"
|
||||
if [[ -z "${manual_filename}" ]]; then
|
||||
echo "[database-import] 无法读取 MANUAL_INPUT_FILE_FILENAME,不能确认手动上传文件名。" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${CONFIRM_INPUT_FILE}" != "${manual_filename}" ]]; then
|
||||
echo "[database-import] CONFIRM_INPUT_FILE 必须与手动上传文件原始文件名一致: ${manual_filename}" >&2
|
||||
exit 1
|
||||
fi
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
def credentialBindings = []
|
||||
if (params.TOKEN_CREDENTIAL_ID?.trim()) {
|
||||
credentialBindings.add(string(credentialsId: params.TOKEN_CREDENTIAL_ID.trim(), variable: 'GENARRATIVE_SPACETIME_TOKEN'))
|
||||
}
|
||||
if (params.BOOTSTRAP_SECRET_CREDENTIAL_ID?.trim()) {
|
||||
credentialBindings.add(string(credentialsId: params.BOOTSTRAP_SECRET_CREDENTIAL_ID.trim(), variable: 'GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET'))
|
||||
}
|
||||
|
||||
def importStep = {
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
|
||||
chmod +x scripts/deploy/maintenance-on.sh scripts/deploy/maintenance-off.sh
|
||||
|
||||
input_path="${EFFECTIVE_INPUT_FILE}"
|
||||
if [[ "${input_path}" != /* ]]; then
|
||||
input_path="${WORKSPACE}/${input_path}"
|
||||
fi
|
||||
if [[ ! -s "${input_path}" ]]; then
|
||||
echo "[database-import] INPUT_FILE 不存在或为空: ${input_path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
backup_dir="${PRE_IMPORT_BACKUP_DIRECTORY}"
|
||||
backup_path="${backup_dir}/${EFFECTIVE_PRE_IMPORT_BACKUP_NAME}"
|
||||
mkdir -p "${backup_dir}"
|
||||
|
||||
completed=0
|
||||
on_exit() {
|
||||
local exit_code=$?
|
||||
if [[ "${exit_code}" -ne 0 && "${completed}" -ne 1 ]]; then
|
||||
echo "[database-import] 导入失败,保持维护模式。导入前备份如已生成,会保留在 ${backup_path}。" >&2
|
||||
fi
|
||||
exit "${exit_code}"
|
||||
}
|
||||
trap on_exit EXIT
|
||||
|
||||
scripts/deploy/maintenance-on.sh "database import ${DATABASE}"
|
||||
|
||||
backup_args=(scripts/spacetime-export-migration-json.mjs --out "${backup_path}" --database "${DATABASE}")
|
||||
import_args=(scripts/spacetime-import-migration-json.mjs --in "${input_path}" --database "${DATABASE}")
|
||||
for args_name in backup_args import_args; do
|
||||
declare -n current_args="${args_name}"
|
||||
# server-url 明确指向目标实例时,不再同时透传默认 alias,避免 CLI 授权与 HTTP 导入落到不同目标。
|
||||
if [[ -n "${SPACETIME_SERVER_URL}" ]]; then
|
||||
current_args+=(--server-url "${SPACETIME_SERVER_URL}")
|
||||
elif [[ -n "${SPACETIME_SERVER}" ]]; then
|
||||
current_args+=(--server "${SPACETIME_SERVER}")
|
||||
fi
|
||||
if [[ -n "${SPACETIME_ROOT_DIR}" ]]; then
|
||||
current_args+=(--root-dir "${SPACETIME_ROOT_DIR}")
|
||||
fi
|
||||
done
|
||||
|
||||
backup_args+=(--note "jenkins pre-import backup ${BUILD_TAG}")
|
||||
node "${backup_args[@]}"
|
||||
test -s "${backup_path}"
|
||||
sha256sum "${backup_path}" >"${backup_path}.sha256"
|
||||
|
||||
if [[ -n "${SERVER_BACKUP_DIRECTORY}" ]]; then
|
||||
mkdir -p "${SERVER_BACKUP_DIRECTORY}"
|
||||
install -m 0640 "${backup_path}" "${SERVER_BACKUP_DIRECTORY}/${EFFECTIVE_PRE_IMPORT_BACKUP_NAME}"
|
||||
install -m 0640 "${backup_path}.sha256" "${SERVER_BACKUP_DIRECTORY}/${EFFECTIVE_PRE_IMPORT_BACKUP_NAME}.sha256"
|
||||
fi
|
||||
|
||||
if [[ -n "${INCLUDE_TABLES}" ]]; then
|
||||
import_args+=(--include "${INCLUDE_TABLES}")
|
||||
fi
|
||||
if [[ -n "${CHUNK_SIZE}" ]]; then
|
||||
import_args+=(--chunk-size "${CHUNK_SIZE}")
|
||||
fi
|
||||
if [[ "${DRY_RUN}" == "true" ]]; then
|
||||
import_args+=(--dry-run)
|
||||
fi
|
||||
if [[ "${INCREMENTAL}" == "true" ]]; then
|
||||
import_args+=(--incremental)
|
||||
fi
|
||||
if [[ "${REPLACE_EXISTING}" == "true" ]]; then
|
||||
import_args+=(--replace-existing)
|
||||
fi
|
||||
import_args+=(--note "jenkins database import ${BUILD_TAG}")
|
||||
|
||||
node "${import_args[@]}"
|
||||
|
||||
# 导入成功后只做本机健康检查;业务级数据核验仍以迁移脚本的表级统计为准。
|
||||
if [[ "${RUN_SMOKE_TEST}" == "true" && -n "${SMOKE_HEALTH_URL}" ]]; then
|
||||
curl -fsS --max-time 10 "${SMOKE_HEALTH_URL}" >/dev/null
|
||||
fi
|
||||
|
||||
scripts/deploy/maintenance-off.sh
|
||||
completed=1
|
||||
echo "[database-import] 完成: dry_run=${DRY_RUN}, database=${DATABASE}, source_commit=$(cat .jenkins-source-commit)"
|
||||
'
|
||||
'''
|
||||
}
|
||||
|
||||
if (credentialBindings) {
|
||||
withCredentials(credentialBindings) {
|
||||
importStep()
|
||||
}
|
||||
} else {
|
||||
importStep()
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
archiveArtifacts artifacts: "${env.PRE_IMPORT_BACKUP_DIRECTORY}/${env.EFFECTIVE_PRE_IMPORT_BACKUP_NAME},${env.PRE_IMPORT_BACKUP_DIRECTORY}/${env.EFFECTIVE_PRE_IMPORT_BACKUP_NAME}.sha256", allowEmptyArchive: true, fingerprint: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "数据库导入流水线完成: target=${params.DEPLOY_TARGET}, database=${params.DATABASE}, dryRun=${params.DRY_RUN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
176
jenkins/Jenkinsfile.production-full-build-and-deploy
Normal file
176
jenkins/Jenkinsfile.production-full-build-and-deploy
Normal file
@@ -0,0 +1,176 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
triggers {
|
||||
cron('0 4 * * *')
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
|
||||
}
|
||||
|
||||
parameters {
|
||||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '源码分支,默认 master 最新提交')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,指定属于 SOURCE_BRANCH 的 Git commit')
|
||||
string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER')
|
||||
string(name: 'WEB_BUILD_JOB_NAME', defaultValue: 'Genarrative-Web-Build', description: 'Web 构建流水线作业名')
|
||||
string(name: 'API_BUILD_JOB_NAME', defaultValue: 'Genarrative-Api-Build', description: 'API 构建流水线作业名')
|
||||
string(name: 'STDB_BUILD_JOB_NAME', defaultValue: 'Genarrative-Stdb-Module-Build', description: 'Stdb 构建流水线作业名')
|
||||
string(name: 'WEB_DEPLOY_JOB_NAME', defaultValue: 'Genarrative-Web-Deploy', description: 'Web 发布流水线作业名')
|
||||
string(name: 'API_DEPLOY_JOB_NAME', defaultValue: 'Genarrative-Api-Deploy', description: 'API 发布流水线作业名')
|
||||
string(name: 'STDB_PUBLISH_JOB_NAME', defaultValue: 'Genarrative-Stdb-Module-Publish', description: 'Stdb 发布流水线作业名')
|
||||
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: '逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent')
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent;当前 Linux 开发/构建/开发部署 agent 不可冒充 release 部署机')
|
||||
string(name: 'DATABASE', defaultValue: 'genarrative-prod', description: '生产 SpacetimeDB database')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Resolve Source') {
|
||||
agent {
|
||||
label 'linux && genarrative-build'
|
||||
}
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [[$class: 'CleanBeforeCheckout']],
|
||||
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]],
|
||||
])
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/jenkins-checkout-source.sh
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" \
|
||||
COMMIT_HASH="${COMMIT_HASH}" \
|
||||
GIT_REMOTE_URL="${GIT_REMOTE_URL}" \
|
||||
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
|
||||
scripts/jenkins-checkout-source.sh
|
||||
'
|
||||
'''
|
||||
script {
|
||||
env.SOURCE_COMMIT = readFile('.jenkins-source-commit').trim()
|
||||
env.EFFECTIVE_BUILD_VERSION = params.BUILD_VERSION?.trim() ? params.BUILD_VERSION.trim() : env.BUILD_NUMBER
|
||||
if (params.DEPLOY_TARGET == 'release' && !params.CONFIRM_RELEASE_DEPLOY_AGENT) {
|
||||
error('release 部署需要先配置独立 release 部署 agent,并勾选 CONFIRM_RELEASE_DEPLOY_AGENT。当前 Linux 开发/构建/开发部署 agent 不能执行 release 部署。')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build Components') {
|
||||
parallel {
|
||||
stage('Web') {
|
||||
steps {
|
||||
script {
|
||||
def webRun = build job: params.WEB_BUILD_JOB_NAME,
|
||||
wait: true,
|
||||
propagate: true,
|
||||
parameters: [
|
||||
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH),
|
||||
string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT),
|
||||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
|
||||
]
|
||||
env.WEB_BUILD_NUMBER = webRun.number.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Api') {
|
||||
steps {
|
||||
script {
|
||||
def apiRun = build job: params.API_BUILD_JOB_NAME,
|
||||
wait: true,
|
||||
propagate: true,
|
||||
parameters: [
|
||||
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH),
|
||||
string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT),
|
||||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
|
||||
]
|
||||
env.API_BUILD_NUMBER = apiRun.number.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Stdb') {
|
||||
steps {
|
||||
script {
|
||||
def stdbRun = build job: params.STDB_BUILD_JOB_NAME,
|
||||
wait: true,
|
||||
propagate: true,
|
||||
parameters: [
|
||||
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH),
|
||||
string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT),
|
||||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
|
||||
string(name: 'DATABASE', value: params.DATABASE),
|
||||
]
|
||||
env.STDB_BUILD_NUMBER = stdbRun.number.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy Stdb') {
|
||||
steps {
|
||||
build job: params.STDB_PUBLISH_JOB_NAME,
|
||||
wait: true,
|
||||
propagate: true,
|
||||
parameters: [
|
||||
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH),
|
||||
string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT),
|
||||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
|
||||
string(name: 'DATABASE', value: params.DATABASE),
|
||||
string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET),
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: params.CONFIRM_RELEASE_DEPLOY_AGENT),
|
||||
string(name: 'BUILD_JOB_NAME', value: params.STDB_BUILD_JOB_NAME),
|
||||
string(name: 'BUILD_NUMBER_TO_DEPLOY', value: env.STDB_BUILD_NUMBER),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy Api') {
|
||||
steps {
|
||||
build job: params.API_DEPLOY_JOB_NAME,
|
||||
wait: true,
|
||||
propagate: true,
|
||||
parameters: [
|
||||
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH),
|
||||
string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT),
|
||||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
|
||||
string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET),
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: params.CONFIRM_RELEASE_DEPLOY_AGENT),
|
||||
string(name: 'BUILD_JOB_NAME', value: params.API_BUILD_JOB_NAME),
|
||||
string(name: 'BUILD_NUMBER_TO_DEPLOY', value: env.API_BUILD_NUMBER),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy Web') {
|
||||
steps {
|
||||
build job: params.WEB_DEPLOY_JOB_NAME,
|
||||
wait: true,
|
||||
propagate: true,
|
||||
parameters: [
|
||||
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH),
|
||||
string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT),
|
||||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
|
||||
string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET),
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: params.CONFIRM_RELEASE_DEPLOY_AGENT),
|
||||
string(name: 'BUILD_JOB_NAME', value: params.WEB_BUILD_JOB_NAME),
|
||||
string(name: 'BUILD_NUMBER_TO_DEPLOY', value: env.WEB_BUILD_NUMBER),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "Full Build-And-Deploy 完成: version=${env.EFFECTIVE_BUILD_VERSION}, commit=${env.SOURCE_COMMIT}"
|
||||
}
|
||||
}
|
||||
}
|
||||
241
jenkins/Jenkinsfile.production-server-provision
Normal file
241
jenkins/Jenkinsfile.production-server-provision
Normal file
@@ -0,0 +1,241 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
|
||||
}
|
||||
|
||||
parameters {
|
||||
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: '逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent')
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent')
|
||||
booleanParam(name: 'CONFIRM_PROVISION', defaultValue: false, description: '确认执行服务器初始化;未勾选时只允许 dry-run')
|
||||
booleanParam(name: 'DRY_RUN', defaultValue: true, description: '只打印将执行的服务器初始化命令,不写入系统配置')
|
||||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '部署脚本来源分支')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '部署脚本来源 commit')
|
||||
string(name: 'SERVER_NAME', defaultValue: 'genarrative.example.com', description: 'Nginx server_name 与证书域名')
|
||||
string(name: 'SPACETIME_BIN_SOURCE', defaultValue: '/usr/local/bin/spacetime', description: '服务器上已有 spacetime CLI 路径')
|
||||
string(name: 'SPACETIME_ROOT', defaultValue: '/stdb', description: 'SpacetimeDB root-dir')
|
||||
string(name: 'RELEASE_ROOT', defaultValue: '/opt/genarrative/releases', description: 'release 根目录')
|
||||
string(name: 'CURRENT_LINK', defaultValue: '/opt/genarrative/current', description: '当前版本软链接')
|
||||
string(name: 'WEB_LINK', defaultValue: '/srv/genarrative/web', description: 'Nginx 静态站点目录或软链接')
|
||||
string(name: 'API_ENV_FILE', defaultValue: '/etc/genarrative/api-server.env', description: 'api-server 环境文件')
|
||||
string(name: 'API_PORT', defaultValue: '8082', description: 'api-server 本机监听端口')
|
||||
booleanParam(name: 'INSTALL_NGINX_CONFIG', defaultValue: true, description: '安装 Nginx 配置并执行 nginx -t')
|
||||
booleanParam(name: 'ENABLE_SERVICES', defaultValue: true, description: '启用并启动 spacetimedb 与 api-server systemd 服务')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Prepare') {
|
||||
agent {
|
||||
label 'linux && genarrative-build'
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
if (params.DEPLOY_TARGET == 'release' && !params.CONFIRM_RELEASE_DEPLOY_AGENT) {
|
||||
error('release provision 需要先配置独立 release 部署 agent,并勾选 CONFIRM_RELEASE_DEPLOY_AGENT。')
|
||||
}
|
||||
if (!params.DRY_RUN && !params.CONFIRM_PROVISION) {
|
||||
error('执行服务器初始化前必须勾选 CONFIRM_PROVISION;否则请保持 DRY_RUN=true。')
|
||||
}
|
||||
if (!params.SERVER_NAME?.trim()) {
|
||||
error('SERVER_NAME 不能为空。')
|
||||
}
|
||||
if (!params.SPACETIME_BIN_SOURCE?.trim()) {
|
||||
error('SPACETIME_BIN_SOURCE 不能为空。')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Checkout Provision Files') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [[$class: 'CleanBeforeCheckout']],
|
||||
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]],
|
||||
])
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/jenkins-checkout-source.sh
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" \
|
||||
COMMIT_HASH="${COMMIT_HASH}" \
|
||||
GIT_REMOTE_URL="${GIT_REMOTE_URL}" \
|
||||
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
|
||||
scripts/jenkins-checkout-source.sh
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Provision Server') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
|
||||
require_path() {
|
||||
local path="$1"
|
||||
if [[ ! -e "${path}" ]]; then
|
||||
echo "[server-provision] 缺少必要文件: ${path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_cmd() {
|
||||
echo "+ $*"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
install_file() {
|
||||
local source="$1"
|
||||
local target="$2"
|
||||
local mode="$3"
|
||||
echo "+ install -m ${mode} ${source} ${target}"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
install -m "${mode}" "${source}" "${target}"
|
||||
fi
|
||||
}
|
||||
|
||||
render_nginx_config() {
|
||||
sed "s/genarrative.example.com/${SERVER_NAME}/g" deploy/nginx/genarrative.conf
|
||||
}
|
||||
|
||||
render_api_env_example() {
|
||||
sed \
|
||||
-e "s|^GENARRATIVE_API_PORT=.*|GENARRATIVE_API_PORT=${API_PORT}|" \
|
||||
-e "s|^GENARRATIVE_SPACETIME_SERVER_URL=.*|GENARRATIVE_SPACETIME_SERVER_URL=http://127.0.0.1:3000|" \
|
||||
deploy/env/api-server.env.example
|
||||
}
|
||||
|
||||
escape_sed_replacement() {
|
||||
printf "%s" "$1" | sed "s/[&|]/\\\\&/g"
|
||||
}
|
||||
|
||||
render_spacetimedb_service() {
|
||||
local root_escaped
|
||||
root_escaped="$(escape_sed_replacement "${SPACETIME_ROOT}")"
|
||||
sed \
|
||||
-e "s|/stdb|${root_escaped}|g" \
|
||||
deploy/systemd/spacetimedb.service
|
||||
}
|
||||
|
||||
render_api_service() {
|
||||
local current_escaped env_escaped
|
||||
current_escaped="$(escape_sed_replacement "${CURRENT_LINK}")"
|
||||
env_escaped="$(escape_sed_replacement "${API_ENV_FILE}")"
|
||||
sed \
|
||||
-e "s|/opt/genarrative/current|${current_escaped}|g" \
|
||||
-e "s|/etc/genarrative/api-server.env|${env_escaped}|g" \
|
||||
deploy/systemd/genarrative-api.service
|
||||
}
|
||||
|
||||
require_path deploy/systemd/spacetimedb.service
|
||||
require_path deploy/systemd/genarrative-api.service
|
||||
require_path deploy/nginx/genarrative.conf
|
||||
require_path deploy/nginx/snippets/genarrative-maintenance.conf
|
||||
require_path deploy/env/api-server.env.example
|
||||
require_path scripts/deploy/maintenance-on.sh
|
||||
require_path scripts/deploy/maintenance-off.sh
|
||||
require_path scripts/deploy/maintenance-status.sh
|
||||
|
||||
echo "[server-provision] target=${DEPLOY_TARGET}, dry_run=${DRY_RUN}, source_commit=$(cat .jenkins-source-commit)"
|
||||
|
||||
run_cmd id
|
||||
run_cmd mkdir -p "${SPACETIME_ROOT}" "${RELEASE_ROOT}" "$(dirname "${CURRENT_LINK}")" "$(dirname "${WEB_LINK}")" /etc/genarrative /var/lib/genarrative/maintenance /var/lib/genarrative/auth
|
||||
|
||||
if ! id spacetimedb >/dev/null 2>&1; then
|
||||
run_cmd useradd --system --home-dir "${SPACETIME_ROOT}" --shell /usr/sbin/nologin spacetimedb
|
||||
else
|
||||
echo "[server-provision] 用户已存在: spacetimedb"
|
||||
fi
|
||||
|
||||
if ! id genarrative >/dev/null 2>&1; then
|
||||
run_cmd useradd --system --home-dir /opt/genarrative --shell /usr/sbin/nologin genarrative
|
||||
else
|
||||
echo "[server-provision] 用户已存在: genarrative"
|
||||
fi
|
||||
|
||||
run_cmd chown -R spacetimedb:spacetimedb "${SPACETIME_ROOT}"
|
||||
run_cmd chown -R genarrative:genarrative /opt/genarrative /var/lib/genarrative /srv/genarrative
|
||||
|
||||
if [[ ! -x "${SPACETIME_BIN_SOURCE}" ]]; then
|
||||
echo "[server-provision] spacetime CLI 不存在或不可执行: ${SPACETIME_BIN_SOURCE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "+ install -m 0755 ${SPACETIME_BIN_SOURCE} ${SPACETIME_ROOT}/spacetime"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
install -m 0755 "${SPACETIME_BIN_SOURCE}" "${SPACETIME_ROOT}/spacetime"
|
||||
chown spacetimedb:spacetimedb "${SPACETIME_ROOT}/spacetime"
|
||||
fi
|
||||
|
||||
spacetimedb_service="$(mktemp)"
|
||||
api_service="$(mktemp)"
|
||||
render_spacetimedb_service >"${spacetimedb_service}"
|
||||
render_api_service >"${api_service}"
|
||||
install_file "${spacetimedb_service}" /etc/systemd/system/spacetimedb.service 0644
|
||||
install_file "${api_service}" /etc/systemd/system/genarrative-api.service 0644
|
||||
rm -f "${spacetimedb_service}" "${api_service}"
|
||||
|
||||
if [[ ! -f "${API_ENV_FILE}" ]]; then
|
||||
echo "+ create ${API_ENV_FILE} from example"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
render_api_env_example >"${API_ENV_FILE}"
|
||||
chmod 0600 "${API_ENV_FILE}"
|
||||
chown root:root "${API_ENV_FILE}"
|
||||
fi
|
||||
else
|
||||
echo "[server-provision] 已存在环境文件,保留不覆盖: ${API_ENV_FILE}"
|
||||
fi
|
||||
|
||||
if [[ "${INSTALL_NGINX_CONFIG}" == "true" ]]; then
|
||||
run_cmd mkdir -p /etc/nginx/snippets /etc/nginx/conf.d
|
||||
echo "+ render deploy/nginx/genarrative.conf -> /etc/nginx/conf.d/genarrative.conf"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
render_nginx_config >/etc/nginx/conf.d/genarrative.conf
|
||||
chmod 0644 /etc/nginx/conf.d/genarrative.conf
|
||||
fi
|
||||
install_file deploy/nginx/snippets/genarrative-maintenance.conf /etc/nginx/snippets/genarrative-maintenance.conf 0644
|
||||
run_cmd nginx -t
|
||||
fi
|
||||
|
||||
run_cmd systemctl daemon-reload
|
||||
if [[ "${ENABLE_SERVICES}" == "true" ]]; then
|
||||
run_cmd systemctl enable spacetimedb.service genarrative-api.service
|
||||
run_cmd systemctl restart spacetimedb.service
|
||||
if [[ -x "${CURRENT_LINK}/api-server" ]]; then
|
||||
run_cmd systemctl restart genarrative-api.service
|
||||
else
|
||||
echo "[server-provision] 尚未发现 ${CURRENT_LINK}/api-server,跳过 api-server 首次启动。后续 API deploy 会重启服务。"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[server-provision] 完成。若是首次初始化,请补齐 ${API_ENV_FILE} 的真实密钥后再启动 api-server。"
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "Server provision 完成: target=${params.DEPLOY_TARGET}, dryRun=${params.DRY_RUN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
107
jenkins/Jenkinsfile.production-stdb-module-build
Normal file
107
jenkins/Jenkinsfile.production-stdb-module-build
Normal file
@@ -0,0 +1,107 @@
|
||||
pipeline {
|
||||
agent {
|
||||
label 'linux && genarrative-build'
|
||||
}
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
|
||||
CARGO_HOME = '/var/cache/genarrative-build/cargo-home'
|
||||
CARGO_TARGET_DIR = '/var/cache/genarrative-build/cargo-target/prod-release'
|
||||
CARGO_INCREMENTAL = '0'
|
||||
RUSTC_WRAPPER = 'sccache'
|
||||
SCCACHE_DIR = '/var/cache/genarrative-build/sccache'
|
||||
SCCACHE_CACHE_SIZE = '30G'
|
||||
}
|
||||
|
||||
parameters {
|
||||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '源码分支,默认 master 最新提交')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,指定属于 SOURCE_BRANCH 的 Git commit')
|
||||
string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER')
|
||||
booleanParam(name: 'PUBLISH_AFTER_BUILD', defaultValue: false, description: '构建成功后是否触发 Stdb module 发布')
|
||||
string(name: 'DEPLOY_JOB_NAME', defaultValue: 'Genarrative-Stdb-Module-Publish', description: 'Stdb module 发布流水线作业名')
|
||||
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: 'PUBLISH_AFTER_BUILD=true 时的逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent')
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: 'PUBLISH_AFTER_BUILD=true 且目标为 release 时必须确认已有独立 release 部署 agent')
|
||||
string(name: 'DATABASE', defaultValue: 'genarrative-prod', description: '生产 SpacetimeDB database')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [[$class: 'CleanBeforeCheckout']],
|
||||
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]],
|
||||
])
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/jenkins-checkout-source.sh
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" \
|
||||
COMMIT_HASH="${COMMIT_HASH}" \
|
||||
GIT_REMOTE_URL="${GIT_REMOTE_URL}" \
|
||||
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
|
||||
scripts/jenkins-checkout-source.sh
|
||||
'
|
||||
'''
|
||||
script {
|
||||
env.SOURCE_COMMIT = readFile('.jenkins-source-commit').trim()
|
||||
env.EFFECTIVE_BUILD_VERSION = params.BUILD_VERSION?.trim() ? params.BUILD_VERSION.trim() : env.BUILD_NUMBER
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build Stdb Module') {
|
||||
steps {
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
mkdir -p "${CARGO_HOME}" "${CARGO_TARGET_DIR}" "${SCCACHE_DIR}"
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" SOURCE_COMMIT="${SOURCE_COMMIT}" \
|
||||
npm run build:production-release -- --component spacetime-module --name "${EFFECTIVE_BUILD_VERSION}"
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Archive') {
|
||||
steps {
|
||||
archiveArtifacts artifacts: "build/${env.EFFECTIVE_BUILD_VERSION}/spacetime_module.wasm,build/${env.EFFECTIVE_BUILD_VERSION}/spacetime_module.wasm.sha256,build/${env.EFFECTIVE_BUILD_VERSION}/release-manifest.json", fingerprint: true
|
||||
}
|
||||
}
|
||||
|
||||
stage('Publish') {
|
||||
when {
|
||||
expression { return params.PUBLISH_AFTER_BUILD }
|
||||
}
|
||||
steps {
|
||||
build job: params.DEPLOY_JOB_NAME,
|
||||
wait: true,
|
||||
propagate: true,
|
||||
parameters: [
|
||||
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH),
|
||||
string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT),
|
||||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
|
||||
string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET),
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: params.CONFIRM_RELEASE_DEPLOY_AGENT),
|
||||
string(name: 'BUILD_JOB_NAME', value: env.JOB_NAME),
|
||||
string(name: 'BUILD_NUMBER_TO_DEPLOY', value: env.BUILD_NUMBER),
|
||||
string(name: 'DATABASE', value: params.DATABASE),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "Stdb module 构建完成: version=${env.EFFECTIVE_BUILD_VERSION}, commit=${env.SOURCE_COMMIT}"
|
||||
}
|
||||
}
|
||||
}
|
||||
122
jenkins/Jenkinsfile.production-stdb-module-publish
Normal file
122
jenkins/Jenkinsfile.production-stdb-module-publish
Normal file
@@ -0,0 +1,122 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
|
||||
}
|
||||
|
||||
parameters {
|
||||
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: '逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent')
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent;当前 Linux 开发/构建/开发部署 agent 不可冒充 release 部署机')
|
||||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '部署脚本来源分支')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '部署脚本来源 commit;上游触发时传实际构建 commit')
|
||||
string(name: 'BUILD_VERSION', defaultValue: '', description: '待发布版本号')
|
||||
string(name: 'BUILD_JOB_NAME', defaultValue: 'Genarrative-Stdb-Module-Build', description: 'Stdb module 构建流水线作业名')
|
||||
string(name: 'BUILD_NUMBER_TO_DEPLOY', defaultValue: '', description: '要复制归档产物的上游构建号')
|
||||
string(name: 'DATABASE', defaultValue: 'genarrative-prod', description: '生产 SpacetimeDB database')
|
||||
string(name: 'SPACETIME_SERVER', defaultValue: 'local', description: 'SpacetimeDB server alias')
|
||||
booleanParam(name: 'CLEAR_DATABASE', defaultValue: false, description: '是否清空数据库后发布')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Prepare') {
|
||||
agent {
|
||||
label 'linux && genarrative-build'
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
if (params.DEPLOY_TARGET == 'release' && !params.CONFIRM_RELEASE_DEPLOY_AGENT) {
|
||||
error('release 部署需要先配置独立 release 部署 agent,并勾选 CONFIRM_RELEASE_DEPLOY_AGENT。当前 Linux 开发/构建/开发部署 agent 不能执行 release 部署。')
|
||||
}
|
||||
if (!params.BUILD_VERSION?.trim()) {
|
||||
error('BUILD_VERSION 不能为空。')
|
||||
}
|
||||
if (!params.BUILD_JOB_NAME?.trim()) {
|
||||
error('BUILD_JOB_NAME 不能为空。')
|
||||
}
|
||||
if (!params.BUILD_NUMBER_TO_DEPLOY?.trim()) {
|
||||
error('BUILD_NUMBER_TO_DEPLOY 不能为空。')
|
||||
}
|
||||
if (!params.DATABASE?.trim()) {
|
||||
error('DATABASE 不能为空。')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Checkout Publish Scripts') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [[$class: 'CleanBeforeCheckout']],
|
||||
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]],
|
||||
])
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/jenkins-checkout-source.sh
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" \
|
||||
COMMIT_HASH="${COMMIT_HASH}" \
|
||||
GIT_REMOTE_URL="${GIT_REMOTE_URL}" \
|
||||
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
|
||||
scripts/jenkins-checkout-source.sh
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Fetch Artifact') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
copyArtifacts(
|
||||
projectName: params.BUILD_JOB_NAME,
|
||||
selector: specific(params.BUILD_NUMBER_TO_DEPLOY),
|
||||
filter: "build/${params.BUILD_VERSION}/spacetime_module.wasm,build/${params.BUILD_VERSION}/spacetime_module.wasm.sha256,build/${params.BUILD_VERSION}/release-manifest.json",
|
||||
target: '.',
|
||||
fingerprintArtifacts: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
stage('Publish Stdb Module') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
def clearArg = params.CLEAR_DATABASE ? '--clear-database' : ''
|
||||
sh """
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/deploy/production-stdb-publish.sh scripts/deploy/maintenance-on.sh scripts/deploy/maintenance-off.sh
|
||||
scripts/deploy/production-stdb-publish.sh \\
|
||||
--source-dir "build/${params.BUILD_VERSION}" \\
|
||||
--database "${params.DATABASE}" \\
|
||||
--server "${params.SPACETIME_SERVER}" \\
|
||||
${clearArg}
|
||||
'
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "Stdb module 发布完成: version=${params.BUILD_VERSION}, database=${params.DATABASE}"
|
||||
}
|
||||
}
|
||||
}
|
||||
104
jenkins/Jenkinsfile.production-web-build
Normal file
104
jenkins/Jenkinsfile.production-web-build
Normal file
@@ -0,0 +1,104 @@
|
||||
pipeline {
|
||||
agent {
|
||||
label 'linux && genarrative-build'
|
||||
}
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
|
||||
}
|
||||
|
||||
parameters {
|
||||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '源码分支,默认 master 最新提交')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,指定属于 SOURCE_BRANCH 的 Git commit')
|
||||
string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER')
|
||||
booleanParam(name: 'RUN_NPM_CI', defaultValue: false, description: '构建前是否执行 npm ci')
|
||||
booleanParam(name: 'PUBLISH_AFTER_BUILD', defaultValue: false, description: '构建成功后是否触发 Web 发布')
|
||||
string(name: 'DEPLOY_JOB_NAME', defaultValue: 'Genarrative-Web-Deploy', description: 'Web 发布流水线作业名')
|
||||
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: 'PUBLISH_AFTER_BUILD=true 时的逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent')
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: 'PUBLISH_AFTER_BUILD=true 且目标为 release 时必须确认已有独立 release 部署 agent')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [[$class: 'CleanBeforeCheckout']],
|
||||
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]],
|
||||
])
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/jenkins-checkout-source.sh
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" \
|
||||
COMMIT_HASH="${COMMIT_HASH}" \
|
||||
GIT_REMOTE_URL="${GIT_REMOTE_URL}" \
|
||||
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
|
||||
scripts/jenkins-checkout-source.sh
|
||||
'
|
||||
'''
|
||||
script {
|
||||
env.SOURCE_COMMIT = readFile('.jenkins-source-commit').trim()
|
||||
env.EFFECTIVE_BUILD_VERSION = params.BUILD_VERSION?.trim() ? params.BUILD_VERSION.trim() : env.BUILD_NUMBER
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build Web') {
|
||||
steps {
|
||||
script {
|
||||
if (params.RUN_NPM_CI) {
|
||||
sh 'bash -lc "npm ci"'
|
||||
}
|
||||
}
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" SOURCE_COMMIT="${SOURCE_COMMIT}" \
|
||||
npm run build:production-release -- --component web --name "${EFFECTIVE_BUILD_VERSION}"
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Archive') {
|
||||
steps {
|
||||
archiveArtifacts artifacts: "build/${env.EFFECTIVE_BUILD_VERSION}/web.tar.gz,build/${env.EFFECTIVE_BUILD_VERSION}/web.tar.gz.sha256,build/${env.EFFECTIVE_BUILD_VERSION}/release-manifest.json", fingerprint: true
|
||||
}
|
||||
}
|
||||
|
||||
stage('Publish') {
|
||||
when {
|
||||
expression { return params.PUBLISH_AFTER_BUILD }
|
||||
}
|
||||
steps {
|
||||
build job: params.DEPLOY_JOB_NAME,
|
||||
wait: true,
|
||||
propagate: true,
|
||||
parameters: [
|
||||
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH),
|
||||
string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT),
|
||||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
|
||||
string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET),
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: params.CONFIRM_RELEASE_DEPLOY_AGENT),
|
||||
string(name: 'BUILD_JOB_NAME', value: env.JOB_NAME),
|
||||
string(name: 'BUILD_NUMBER_TO_DEPLOY', value: env.BUILD_NUMBER),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "Web 构建完成: version=${env.EFFECTIVE_BUILD_VERSION}, commit=${env.SOURCE_COMMIT}"
|
||||
}
|
||||
}
|
||||
}
|
||||
117
jenkins/Jenkinsfile.production-web-deploy
Normal file
117
jenkins/Jenkinsfile.production-web-deploy
Normal file
@@ -0,0 +1,117 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
|
||||
}
|
||||
|
||||
parameters {
|
||||
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: '逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent')
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent;当前 Linux 开发/构建/开发部署 agent 不可冒充 release 部署机')
|
||||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '部署脚本来源分支')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '部署脚本来源 commit;上游触发时传实际构建 commit')
|
||||
string(name: 'BUILD_VERSION', defaultValue: '', description: '待发布版本号')
|
||||
string(name: 'BUILD_JOB_NAME', defaultValue: 'Genarrative-Web-Build', description: 'Web 构建流水线作业名')
|
||||
string(name: 'BUILD_NUMBER_TO_DEPLOY', defaultValue: '', description: '要复制归档产物的上游构建号')
|
||||
string(name: 'RELEASE_ROOT', defaultValue: '/opt/genarrative/releases', description: '生产 release 根目录')
|
||||
string(name: 'CURRENT_LINK', defaultValue: '/opt/genarrative/current', description: '当前版本软链接')
|
||||
string(name: 'WEB_LINK', defaultValue: '/srv/genarrative/web', description: 'Nginx 静态站点软链接')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Prepare') {
|
||||
agent {
|
||||
label 'linux && genarrative-build'
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
if (params.DEPLOY_TARGET == 'release' && !params.CONFIRM_RELEASE_DEPLOY_AGENT) {
|
||||
error('release 部署需要先配置独立 release 部署 agent,并勾选 CONFIRM_RELEASE_DEPLOY_AGENT。当前 Linux 开发/构建/开发部署 agent 不能执行 release 部署。')
|
||||
}
|
||||
if (!params.BUILD_VERSION?.trim()) {
|
||||
error('BUILD_VERSION 不能为空。')
|
||||
}
|
||||
if (!params.BUILD_JOB_NAME?.trim()) {
|
||||
error('BUILD_JOB_NAME 不能为空。')
|
||||
}
|
||||
if (!params.BUILD_NUMBER_TO_DEPLOY?.trim()) {
|
||||
error('BUILD_NUMBER_TO_DEPLOY 不能为空。')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Checkout Deploy Scripts') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [[$class: 'CleanBeforeCheckout']],
|
||||
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]],
|
||||
])
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/jenkins-checkout-source.sh
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" \
|
||||
COMMIT_HASH="${COMMIT_HASH}" \
|
||||
GIT_REMOTE_URL="${GIT_REMOTE_URL}" \
|
||||
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
|
||||
scripts/jenkins-checkout-source.sh
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Fetch Artifact') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
copyArtifacts(
|
||||
projectName: params.BUILD_JOB_NAME,
|
||||
selector: specific(params.BUILD_NUMBER_TO_DEPLOY),
|
||||
filter: "build/${params.BUILD_VERSION}/web.tar.gz,build/${params.BUILD_VERSION}/web.tar.gz.sha256,build/${params.BUILD_VERSION}/release-manifest.json",
|
||||
target: '.',
|
||||
fingerprintArtifacts: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy Web') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/deploy/production-web-deploy.sh
|
||||
scripts/deploy/production-web-deploy.sh \
|
||||
--source-dir "build/${BUILD_VERSION}" \
|
||||
--version "${BUILD_VERSION}" \
|
||||
--release-root "${RELEASE_ROOT}" \
|
||||
--current-link "${CURRENT_LINK}" \
|
||||
--web-link "${WEB_LINK}"
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "Web 发布完成: version=${params.BUILD_VERSION}"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user