249 lines
12 KiB
Plaintext
249 lines
12 KiB
Plaintext
pipeline {
|
||
agent none
|
||
|
||
options {
|
||
disableConcurrentBuilds()
|
||
skipDefaultCheckout(true)
|
||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||
}
|
||
|
||
environment {
|
||
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
|
||
GIT_REMOTE_FALLBACK_URL = 'http://10.2.0.10: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: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins Secret Text 凭据 genarrative-notification-emails 合并发送')
|
||
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: 'http://127.0.0.1:3101', 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 {
|
||
script {
|
||
def checkoutFromRemote = { String remoteUrl ->
|
||
checkout([
|
||
$class: 'GitSCM',
|
||
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
|
||
doGenerateSubmoduleConfigurations: false,
|
||
extensions: [[$class: 'CleanBeforeCheckout']],
|
||
userRemoteConfigs: [[url: remoteUrl]],
|
||
])
|
||
}
|
||
try {
|
||
checkoutFromRemote(env.GIT_REMOTE_URL)
|
||
env.EFFECTIVE_GIT_REMOTE_URL = env.GIT_REMOTE_URL
|
||
} catch (error) {
|
||
echo "Git 主地址拉取失败: ${env.GIT_REMOTE_URL},改用备用地址: ${env.GIT_REMOTE_FALLBACK_URL}"
|
||
checkoutFromRemote(env.GIT_REMOTE_FALLBACK_URL)
|
||
env.EFFECTIVE_GIT_REMOTE_URL = env.GIT_REMOTE_FALLBACK_URL
|
||
}
|
||
}
|
||
sh '''
|
||
bash -lc '
|
||
set -euo pipefail
|
||
chmod +x scripts/jenkins-checkout-source.sh
|
||
SOURCE_BRANCH="${SOURCE_BRANCH:-master}" \
|
||
COMMIT_HASH="${COMMIT_HASH:-}" \
|
||
GIT_REMOTE_URL="${EFFECTIVE_GIT_REMOTE_URL:-${GIT_REMOTE_URL}}" \
|
||
GIT_REMOTE_FALLBACK_URL="${GIT_REMOTE_FALLBACK_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
|
||
|
||
database="${DATABASE:?DATABASE 不能为空}"
|
||
spacetime_server_url="${SPACETIME_SERVER_URL:-}"
|
||
spacetime_server="${SPACETIME_SERVER:-}"
|
||
spacetime_root_dir="${EFFECTIVE_SPACETIME_ROOT_DIR:-}"
|
||
include_tables="${INCLUDE_TABLES:-}"
|
||
server_backup_directory="${EFFECTIVE_SERVER_BACKUP_DIRECTORY:-}"
|
||
export_dir="${WORKSPACE_EXPORT_DIRECTORY:-database-exports}"
|
||
export_name="${EFFECTIVE_EXPORT_NAME:-spacetime-migration-${BUILD_NUMBER:-manual}.json}"
|
||
output_path="${export_dir}/${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 "${spacetime_root_dir}" ]]; then
|
||
args+=(--root-dir "${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 "${server_backup_directory}" ]]; then
|
||
mkdir -p "${server_backup_directory}"
|
||
install -m 0640 "${output_path}" "${server_backup_directory}/${export_name}"
|
||
install -m 0640 "${output_path}.sha256" "${server_backup_directory}/${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 {
|
||
always {
|
||
script {
|
||
def notificationParameters = [
|
||
string(name: 'SOURCE_JOB_NAME', value: env.JOB_NAME),
|
||
string(name: 'SOURCE_BUILD_NUMBER', value: env.BUILD_NUMBER),
|
||
string(name: 'SOURCE_BUILD_URL', value: env.BUILD_URL ?: ''),
|
||
string(name: 'SOURCE_RESULT', value: currentBuild.currentResult ?: 'UNKNOWN'),
|
||
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH ?: ''),
|
||
string(name: 'SOURCE_COMMIT', value: env.SOURCE_COMMIT ?: (params.COMMIT_HASH ?: '')),
|
||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION ?: (params.BUILD_VERSION ?: '')),
|
||
string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET ?: ''),
|
||
string(name: 'DATABASE', value: params.DATABASE ?: ''),
|
||
string(name: 'SUMMARY', value: '数据库导出流水线结束'),
|
||
]
|
||
def notificationRecipients = params.NOTIFICATION_EMAILS?.trim()
|
||
if (notificationRecipients) {
|
||
notificationParameters.add(string(name: 'EMAIL_RECIPIENTS', value: notificationRecipients))
|
||
}
|
||
try {
|
||
build job: 'Genarrative-Notify-Email',
|
||
wait: false,
|
||
propagate: false,
|
||
parameters: notificationParameters
|
||
} catch (error) {
|
||
echo "邮件通知触发失败: ${error.message}"
|
||
}
|
||
}
|
||
}
|
||
success {
|
||
echo "数据库导出完成: target=${params.DEPLOY_TARGET}, database=${params.DATABASE}, file=${env.EFFECTIVE_EXPORT_NAME}"
|
||
}
|
||
}
|
||
}
|