Files
Genarrative/jenkins/Jenkinsfile.production-database-export

228 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'
}
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 全局环境变量 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: '', description: '显式 SpacetimeDB server URL填写后优先于 SPACETIME_SERVER')
string(name: 'SPACETIME_ROOT_DIR', defaultValue: '/stdb', description: 'spacetime CLI root-dirrelease 自托管默认 /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:-master}" \
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 {
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}"
}
}
}