Files
WonderQ-Project/Jenkinsfile
T
2026-08-25 10:19:01 +08:00

237 lines
8.8 KiB
Groovy

pipeline {
agent any
options {
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '20'))
skipDefaultCheckout(true)
}
parameters {
string(
name: 'ACR_REGISTRY',
defaultValue: '',
description: 'ACR 登录域名,不包含 https://,优先使用与 ACK 同 VPC 的内网域名'
)
string(name: 'ACR_NAMESPACE', defaultValue: 'wonderq', description: 'ACR 命名空间')
string(name: 'API_REPOSITORY', defaultValue: 'wonderq-api', description: 'API 镜像仓库名')
string(name: 'ADMIN_REPOSITORY', defaultValue: 'wonderq-admin-ui', description: 'Admin UI 镜像仓库名')
string(name: 'ACK_NAMESPACE', defaultValue: 'wonderq-prod', description: 'ACK Kubernetes 命名空间')
string(name: 'API_DEPLOYMENT', defaultValue: 'wonderq-api', description: 'API Deployment 名称')
string(name: 'API_CONTAINER', defaultValue: 'api', description: 'API Deployment 中的容器名称')
string(name: 'ADMIN_DEPLOYMENT', defaultValue: 'wonderq-admin-ui', description: 'Admin UI Deployment 名称')
string(name: 'ADMIN_CONTAINER', defaultValue: 'admin-ui', description: 'Admin UI Deployment 中的容器名称')
string(
name: 'ADMIN_API_BASE_URL',
defaultValue: '',
description: 'Admin UI 构建时的 API 地址;ALB 使用同域名 /api 分流时保持为空'
)
string(
name: 'ACR_CREDENTIALS_ID',
defaultValue: 'acr-push-credential',
description: 'Jenkins 中 ACR 用户名密码 Credential ID'
)
string(
name: 'ACK_KUBECONFIG_CREDENTIALS_ID',
defaultValue: 'ack-prod-kubeconfig',
description: 'Jenkins 中 ACK kubeconfig Secret File Credential ID'
)
booleanParam(name: 'DEPLOY_TO_ACK', defaultValue: true, description: '推送镜像后是否更新 ACK Deployment')
}
environment {
DOCKER_BUILDKIT = '1'
}
stages {
stage('Checkout') {
steps {
checkout scm
script {
def commit = sh(script: 'git rev-parse --short=12 HEAD', returnStdout: true).trim()
env.IMAGE_TAG = "${commit}-${env.BUILD_NUMBER}"
env.API_IMAGE = "${params.ACR_REGISTRY}/${params.ACR_NAMESPACE}/${params.API_REPOSITORY}"
env.ADMIN_IMAGE = "${params.ACR_REGISTRY}/${params.ACR_NAMESPACE}/${params.ADMIN_REPOSITORY}"
}
}
}
stage('Validate') {
steps {
sh '''
set -eu
if [ -z "$ACR_REGISTRY" ]; then
echo "ACR_REGISTRY 不能为空" >&2
exit 1
fi
case "$ACR_REGISTRY" in
*://*)
echo "ACR_REGISTRY 只填写域名,不要包含协议" >&2
exit 1
;;
esac
command -v docker >/dev/null
docker version >/dev/null
if [ "$DEPLOY_TO_ACK" = "true" ]; then
command -v kubectl >/dev/null
fi
'''
}
}
stage('Build API') {
steps {
sh '''
set -eu
docker build \
--tag "$API_IMAGE:$IMAGE_TAG" \
WonderQ-Admin
'''
}
}
stage('Test API') {
steps {
sh '''
set -eu
test_container=''
cleanup_test_container() {
if [ -n "$test_container" ]; then
docker rm --force "$test_container" >/dev/null 2>&1 || true
fi
}
trap cleanup_test_container EXIT
test_container="$(docker create \
--entrypoint python \
"$API_IMAGE:$IMAGE_TAG" \
-m pytest)"
docker cp WonderQ-Admin/tests/. "$test_container:/app/tests"
docker start --attach "$test_container"
'''
}
}
stage('Build Admin UI') {
steps {
sh '''
set -eu
docker build \
--build-arg "VITE_API_BASE_URL=$ADMIN_API_BASE_URL" \
--tag "$ADMIN_IMAGE:$IMAGE_TAG" \
WonderQ-Admin-UI
docker run --rm \
--entrypoint nginx \
"$ADMIN_IMAGE:$IMAGE_TAG" \
-t
'''
}
}
stage('Push Images') {
steps {
withCredentials([
usernamePassword(
credentialsId: params.ACR_CREDENTIALS_ID,
usernameVariable: 'ACR_USERNAME',
passwordVariable: 'ACR_PASSWORD'
)
]) {
sh '''
set -eu
set +x
docker_config="$(mktemp -d)"
export DOCKER_CONFIG="$docker_config"
cleanup_docker_config() {
docker logout "$ACR_REGISTRY" >/dev/null 2>&1 || true
rm -rf -- "$docker_config"
}
trap cleanup_docker_config EXIT
printf '%s' "$ACR_PASSWORD" | docker login \
--username "$ACR_USERNAME" \
--password-stdin \
"$ACR_REGISTRY"
docker push "$API_IMAGE:$IMAGE_TAG"
docker push "$ADMIN_IMAGE:$IMAGE_TAG"
'''
}
}
}
stage('Deploy ACK') {
when {
expression { params.DEPLOY_TO_ACK }
}
steps {
withCredentials([
file(
credentialsId: params.ACK_KUBECONFIG_CREDENTIALS_ID,
variable: 'ACK_KUBECONFIG'
)
]) {
sh '''
set -eu
kubectl --kubeconfig "$ACK_KUBECONFIG" \
--namespace "$ACK_NAMESPACE" \
get deployment "$API_DEPLOYMENT" >/dev/null
kubectl --kubeconfig "$ACK_KUBECONFIG" \
--namespace "$ACK_NAMESPACE" \
get deployment "$ADMIN_DEPLOYMENT" >/dev/null
kubectl --kubeconfig "$ACK_KUBECONFIG" \
--namespace "$ACK_NAMESPACE" \
set image "deployment/$API_DEPLOYMENT" \
"$API_CONTAINER=$API_IMAGE:$IMAGE_TAG"
if ! kubectl --kubeconfig "$ACK_KUBECONFIG" \
--namespace "$ACK_NAMESPACE" \
rollout status "deployment/$API_DEPLOYMENT" --timeout=300s; then
kubectl --kubeconfig "$ACK_KUBECONFIG" \
--namespace "$ACK_NAMESPACE" \
rollout undo "deployment/$API_DEPLOYMENT" || true
exit 1
fi
kubectl --kubeconfig "$ACK_KUBECONFIG" \
--namespace "$ACK_NAMESPACE" \
set image "deployment/$ADMIN_DEPLOYMENT" \
"$ADMIN_CONTAINER=$ADMIN_IMAGE:$IMAGE_TAG"
if ! kubectl --kubeconfig "$ACK_KUBECONFIG" \
--namespace "$ACK_NAMESPACE" \
rollout status "deployment/$ADMIN_DEPLOYMENT" --timeout=300s; then
kubectl --kubeconfig "$ACK_KUBECONFIG" \
--namespace "$ACK_NAMESPACE" \
rollout undo "deployment/$ADMIN_DEPLOYMENT" || true
kubectl --kubeconfig "$ACK_KUBECONFIG" \
--namespace "$ACK_NAMESPACE" \
rollout undo "deployment/$API_DEPLOYMENT" || true
exit 1
fi
'''
}
}
}
}
post {
success {
echo "部署完成,镜像标签:${env.IMAGE_TAG}"
}
failure {
echo '流水线失败,请查看对应阶段日志;数据库迁移不会随 Deployment 回滚自动撤销。'
}
}
}