Files
WonderQ-Project/Jenkinsfile
2026-08-27 16:21:31 +08:00

238 lines
8.9 KiB
Groovy
Raw Permalink 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 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 cp WonderQ-Admin/docker-compose.yml "$test_container:/app/docker-compose.yml"
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-Vue
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 回滚自动撤销。'
}
}
}