Files
Genarrative/jenkins/Jenkinsfile.production-database-export
T
kdletters c8dd6cbd33 统一 Jenkins 本机 Git 源
将生产流水线 Git checkout 地址统一为 127.0.0.1:2222
数据库导入导出改由 Built-In Node 准备并传递脚本
新增 Git 源与目标节点边界的生产运维门禁
同步生产运维文档和项目共享记忆
2026-07-14 22:03:32 +08:00

278 lines
14 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 = 'ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git'
GIT_REMOTE_CREDENTIAL_ID = 'genarrative-local-gitea-ssh'
GIT_REMOTE_FALLBACK_URL = ''
GIT_REMOTE_FALLBACK_CREDENTIAL_ID = ''
}
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-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 ?: ''
}
script {
def checkoutFromRemote = { String remoteUrl, String credentialsId ->
def remoteConfig = [url: remoteUrl, refspec: "+refs/heads/${params.SOURCE_BRANCH}:refs/remotes/origin/${params.SOURCE_BRANCH}"]
def checkoutCredentialId = credentialsId?.trim()
if (checkoutCredentialId) {
remoteConfig.credentialsId = checkoutCredentialId
}
checkout([
$class: 'GitSCM',
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
doGenerateSubmoduleConfigurations: false,
extensions: [
[$class: 'CleanBeforeCheckout'],
[$class: 'CloneOption', shallow: true, depth: 1, noTags: true, timeout: 30, honorRefspec: true],
],
userRemoteConfigs: [remoteConfig],
])
}
try {
checkoutFromRemote(env.GIT_REMOTE_URL, env.GIT_REMOTE_CREDENTIAL_ID)
env.EFFECTIVE_GIT_REMOTE_URL = env.GIT_REMOTE_URL
} catch (error) {
def fallbackRemoteUrl = env.GIT_REMOTE_FALLBACK_URL?.trim()
if (!fallbackRemoteUrl) {
throw error
}
echo "Git 主地址拉取失败: ${env.GIT_REMOTE_URL},改用备用地址: ${fallbackRemoteUrl}"
checkoutFromRemote(fallbackRemoteUrl, env.GIT_REMOTE_FALLBACK_CREDENTIAL_ID)
env.EFFECTIVE_GIT_REMOTE_URL = fallbackRemoteUrl
}
}
withCredentials([sshUserPrivateKey(credentialsId: env.GIT_REMOTE_CREDENTIAL_ID, keyFileVariable: 'GENARRATIVE_GIT_SSH_KEY')]) {
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="" \
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
GENARRATIVE_JENKINS_REUSE_EXISTING_CHECKOUT="true" \
GIT_SSH_COMMAND="ssh -i ${GENARRATIVE_GIT_SSH_KEY:?GENARRATIVE_GIT_SSH_KEY 不能为空} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" \
scripts/jenkins-checkout-source.sh
'
'''
}
script {
env.SOURCE_COMMIT = readFile('.jenkins-source-commit').trim()
}
stash name: 'database-export-source',
includes: '.jenkins-source-commit,scripts/jenkins-prepare-toolchain-env.sh,scripts/deploy/maintenance-on.sh,scripts/deploy/maintenance-off.sh,scripts/spacetime-export-migration-json.mjs,scripts/spacetime-migration-common.mjs',
allowEmpty: false
}
}
stage('Export Database') {
agent {
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
}
steps {
deleteDir()
unstash 'database-export-source'
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/jenkins-prepare-toolchain-env.sh
source scripts/jenkins-prepare-toolchain-env.sh
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}"
}
}
}