增加jenkins打包文件

This commit is contained in:
andy
2026-08-25 10:19:01 +08:00
parent d42f69bd47
commit ed5cfe08b8
4 changed files with 308 additions and 0 deletions

236
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,236 @@
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 回滚自动撤销。'
}
}
}

View File

@@ -0,0 +1,10 @@
node_modules/
dist/
.git/
.idea/
.env
.env.*
coverage/
.vite/
*.log

View File

@@ -0,0 +1,29 @@
# syntax=docker/dockerfile:1
FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY package.json yarn.lock ./
# The project keeps Windows native packages for local Yarn 1 installs. The lock
# file also contains the matching Linux packages used by this container build.
RUN yarn install --frozen-lockfile --non-interactive --ignore-platform
COPY . .
ARG VITE_API_BASE_URL=""
ENV VITE_API_BASE_URL="${VITE_API_BASE_URL}"
RUN yarn build
FROM nginx:1.28-alpine AS runtime
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -q -O /dev/null http://127.0.0.1/healthz || exit 1

View File

@@ -0,0 +1,33 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root /usr/share/nginx/html;
index index.html;
charset utf-8;
server_tokens off;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
gzip on;
gzip_min_length 1024;
gzip_types text/css application/javascript application/json image/svg+xml;
location = /healthz {
access_log off;
default_type text/plain;
return 200 "ok\n";
}
location = /index.html {
expires -1;
try_files $uri =404;
}
location / {
try_files $uri $uri/ /index.html;
}
}