ci: per-service version files, DB migration support ready

- Each service now has its own VERSION.<service> file (all at 1.0.0)
- Jenkinsfile bumps each service's version independently
- versions.json updated per-service (no cross-service overwrite)
- Summary log shows all service:version pairs on success

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-06-11 19:26:46 +08:00
父节点 5593ad790e
当前提交 e774c4ef25
共有 8 个文件被更改,包括 60 次插入43 次删除

96
Jenkinsfile vendored
查看文件

@ -5,20 +5,19 @@ pipeline {
choice( choice(
name: 'SERVICE', name: 'SERVICE',
choices: ['all', 'tenant-service', 'im-service', 'push-service', 'update-service', 'file-service', 'license-service', 'demo-service'], choices: ['all', 'tenant-service', 'im-service', 'push-service', 'update-service', 'file-service', 'license-service', 'demo-service'],
description: '要构建的服务模块all = 全部,包含所有微服务' description: '要构建的服务模块all = 全部,每个服务独立版本号'
) )
} }
environment { environment {
ACR_REGISTRY = 'crpi-n44qjpuucgjt8e8c.cn-beijing.personal.cr.aliyuncs.com' ACR_REGISTRY = 'crpi-n44qjpuucgjt8e8c.cn-beijing.personal.cr.aliyuncs.com'
ACR_NAMESPACE = 'xuqmgroup' ACR_NAMESPACE = 'xuqmgroup'
ACR_USERNAME = 'xuqinmin12' ACR_USERNAME = 'xuqinmin12'
PROD_HOST = '106.54.23.149' PROD_HOST = '106.54.23.149'
PROD_USER = 'ubuntu' PROD_USER = 'ubuntu'
COMPOSE_FILE = '/opt/xuqm/deploy/compose.production.yaml' COMPOSE_FILE = '/opt/xuqm/deploy/compose.production.yaml'
VERSIONS_FILE = '/opt/xuqm/deploy/versions.json' VERSIONS_FILE = '/opt/xuqm/deploy/versions.json'
DOCKER_BUILDKIT = '1' DOCKER_BUILDKIT = '1'
VERSION_FILE = 'VERSION'
} }
options { options {
@ -39,16 +38,25 @@ pipeline {
} }
} }
stage('Resolve Version') { stage('Resolve Versions') {
steps { steps {
script { script {
def current = fileExists(env.VERSION_FILE) ? readFile(env.VERSION_FILE).trim() : '1.0.0' def allServices = ['tenant-service', 'im-service', 'push-service', 'update-service', 'file-service', 'license-service', 'demo-service']
def parts = current.tokenize('.') def targets = params.SERVICE == 'all' ? allServices : [params.SERVICE]
while (parts.size() < 3) parts.add('0')
def newVer = "${parts[0]}.${parts[1]}.${parts[2].toInteger() + 1}" // serviceVersions: Map<svcName, newVersion>
env.SERVICE_VERSION = newVer def serviceVersions = [:]
writeFile file: env.VERSION_FILE, text: newVer for (svc in targets) {
echo "Server: ${current} → ${newVer}" def vf = "VERSION.${svc}"
def current = fileExists(vf) ? readFile(vf).trim() : '1.0.0'
def parts = current.tokenize('.')
while (parts.size() < 3) parts.add('0')
def newVer = "${parts[0]}.${parts[1]}.${parts[2].toInteger() + 1}"
serviceVersions[svc] = newVer
writeFile file: vf, text: newVer
echo "${svc}: ${current} → ${newVer}"
}
env.SERVICE_VERSIONS_JSON = groovy.json.JsonOutput.toJson(serviceVersions)
} }
} }
} }
@ -57,20 +65,19 @@ pipeline {
steps { steps {
withCredentials([string(credentialsId: 'ACR_PASSWORD', variable: 'ACR_PASS')]) { withCredentials([string(credentialsId: 'ACR_PASSWORD', variable: 'ACR_PASS')]) {
script { script {
def services = params.SERVICE == 'all' def serviceVersions = new groovy.json.JsonSlurper().parseText(env.SERVICE_VERSIONS_JSON)
? ['tenant-service', 'im-service', 'push-service', 'update-service', 'file-service', 'license-service', 'demo-service'] for (entry in serviceVersions) {
: [params.SERVICE] def svc = entry.key
def ver = entry.value
for (svc in services) {
def base = "${ACR_REGISTRY}/${ACR_NAMESPACE}/${svc}" def base = "${ACR_REGISTRY}/${ACR_NAMESPACE}/${svc}"
def versionedImage = "${base}:${env.SERVICE_VERSION}" def versionedImage = "${base}:${ver}"
def latestImage = "${base}:latest" def latestImage = "${base}:latest"
echo "Building ${svc} ${env.SERVICE_VERSION}..." echo "Building ${svc}:${ver}..."
bat """ bat """
docker login ${ACR_REGISTRY} -u ${ACR_USERNAME} -p %ACR_PASS% docker login ${ACR_REGISTRY} -u ${ACR_USERNAME} -p %ACR_PASS%
if %errorlevel% neq 0 exit /b 1 if %errorlevel% neq 0 exit /b 1
docker pull --platform=linux/amd64 ${latestImage} || echo Pull failed, will build fresh docker pull --platform=linux/amd64 ${latestImage} || echo Pull failed, will build fresh
docker build --platform=linux/amd64 --build-arg SERVICE_MODULE=${svc} --build-arg SERVICE_VERSION=${env.SERVICE_VERSION} --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from ${latestImage} -t ${versionedImage} -t ${latestImage} . docker build --platform=linux/amd64 --build-arg SERVICE_MODULE=${svc} --build-arg SERVICE_VERSION=${ver} --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from ${latestImage} -t ${versionedImage} -t ${latestImage} .
if %errorlevel% neq 0 exit /b 1 if %errorlevel% neq 0 exit /b 1
docker push ${versionedImage} docker push ${versionedImage}
if %errorlevel% neq 0 exit /b 1 if %errorlevel% neq 0 exit /b 1
@ -90,11 +97,9 @@ pipeline {
lock('prod-deploy') { lock('prod-deploy') {
withCredentials([sshUserPrivateKey(credentialsId: 'PROD_SSH_KEY', keyFileVariable: 'SSH_KEY')]) { withCredentials([sshUserPrivateKey(credentialsId: 'PROD_SSH_KEY', keyFileVariable: 'SSH_KEY')]) {
script { script {
def services = params.SERVICE == 'all' def serviceVersions = new groovy.json.JsonSlurper().parseText(env.SERVICE_VERSIONS_JSON)
? ['tenant-service', 'im-service', 'push-service', 'update-service', 'file-service', 'license-service', 'demo-service'] for (entry in serviceVersions) {
: [params.SERVICE] def svc = entry.key
for (svc in services) {
def latestImage = "${ACR_REGISTRY}/${ACR_NAMESPACE}/${svc}:latest" def latestImage = "${ACR_REGISTRY}/${ACR_NAMESPACE}/${svc}:latest"
echo "Deploying ${svc}..." echo "Deploying ${svc}..."
retry(2) { retry(2) {
@ -104,23 +109,19 @@ pipeline {
} }
} }
// Update versions.json on server for deployed services // 合并更新 versions.json只改动本次构建涉及的服务,不覆盖其它服务或 web 条目)
def deployedServices = params.SERVICE == 'all'
? ['tenant-service', 'im-service', 'push-service', 'update-service', 'file-service', 'license-service', 'demo-service']
: [params.SERVICE]
def releasedAt = new Date().format("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone('UTC')) def releasedAt = new Date().format("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone('UTC'))
def serviceEntries = deployedServices.collect { svc -> def serviceEntries = serviceVersions.collect { svc, ver ->
"d.setdefault('services', {})['${svc}'] = {'version': '${env.SERVICE_VERSION}', 'changed': True}" "d.setdefault('services', {})['${svc}'] = {'version': '${ver}'}"
}.join('\n') }.join('\n')
def updateScript = """\ def updateScript = """\
import json, os import json, os
path = '${env.VERSIONS_FILE}' path = '${env.VERSIONS_FILE}'
d = json.load(open(path)) if os.path.exists(path) else {} d = json.load(open(path)) if os.path.exists(path) else {}
${serviceEntries} ${serviceEntries}
d['serviceVersion'] = '${env.SERVICE_VERSION}'
d['releasedAt'] = '${releasedAt}' d['releasedAt'] = '${releasedAt}'
json.dump(d, open(path, 'w'), indent=2, ensure_ascii=False) json.dump(d, open(path, 'w'), indent=2, ensure_ascii=False)
print('versions.json updated: ${params.SERVICE} = ${env.SERVICE_VERSION}') print('versions.json updated')
""".stripIndent() """.stripIndent()
writeFile file: 'update_versions.py', text: updateScript writeFile file: 'update_versions.py', text: updateScript
bat """ bat """
@ -133,14 +134,17 @@ print('versions.json updated: ${params.SERVICE} = ${env.SERVICE_VERSION}')
} }
} }
stage('Commit Version') { stage('Commit Versions') {
steps { steps {
script { script {
def serviceVersions = new groovy.json.JsonSlurper().parseText(env.SERVICE_VERSIONS_JSON)
def versionFiles = serviceVersions.keySet().collect { "VERSION.${it}" }.join(' ')
def summary = serviceVersions.collect { svc, ver -> "${svc}=${ver}" }.join(', ')
bat """ bat """
git config user.email "jenkins@xuqm.com" git config user.email "jenkins@xuqm.com"
git config user.name "Jenkins CI" git config user.name "Jenkins CI"
git add ${env.VERSION_FILE} git add ${versionFiles}
git diff --cached --quiet || git commit -m "ci: bump server to ${env.SERVICE_VERSION} [skip ci]" git diff --cached --quiet || git commit -m "ci: bump versions [${summary}] [skip ci]"
git push origin HEAD:main git push origin HEAD:main
""" """
} }
@ -149,7 +153,13 @@ print('versions.json updated: ${params.SERVICE} = ${env.SERVICE_VERSION}')
} }
post { post {
success { echo "✅ ${params.SERVICE} v${env.SERVICE_VERSION} 构建部署成功" } success {
failure { echo "❌ 构建失败,请检查日志" } script {
def serviceVersions = new groovy.json.JsonSlurper().parseText(env.SERVICE_VERSIONS_JSON ?: '{}')
def summary = serviceVersions.collect { svc, ver -> "${svc}:${ver}" }.join(', ')
echo "✅ 构建部署成功 — ${summary}"
}
}
failure { echo "❌ 构建失败,请检查日志" }
} }
} }

1
VERSION.demo-service 普通文件
查看文件

@ -0,0 +1 @@
1.0.0

1
VERSION.file-service 普通文件
查看文件

@ -0,0 +1 @@
1.0.0

1
VERSION.im-service 普通文件
查看文件

@ -0,0 +1 @@
1.0.0

1
VERSION.license-service 普通文件
查看文件

@ -0,0 +1 @@
1.0.0

1
VERSION.push-service 普通文件
查看文件

@ -0,0 +1 @@
1.0.0

1
VERSION.tenant-service 普通文件
查看文件

@ -0,0 +1 @@
1.0.0

1
VERSION.update-service 普通文件
查看文件

@ -0,0 +1 @@
1.0.0