diff --git a/Dockerfile.ops b/Dockerfile.ops new file mode 100644 index 0000000..3bf0f78 --- /dev/null +++ b/Dockerfile.ops @@ -0,0 +1,27 @@ +# syntax=docker/dockerfile:1.7 +FROM node:22-alpine AS build +WORKDIR /workspace + +COPY package.json ./package.json +COPY yarn.lock ./yarn.lock +COPY ops-platform ./ops-platform + +ENV YARN_CACHE_FOLDER=/var/cache/yarn + +RUN --mount=type=cache,target=/var/cache/yarn,sharing=locked \ + yarn install --frozen-lockfile + +ARG OPS_APP_BASE=/ +ARG OPS_API_BASE_URL=/api + +RUN cd ops-platform && \ + VITE_APP_BASE=${OPS_APP_BASE} \ + VITE_API_BASE_URL=${OPS_API_BASE_URL} \ + yarn build + +FROM nginx:1.27-alpine + +COPY nginx/ops.conf /etc/nginx/conf.d/default.conf +COPY --from=build /workspace/ops-platform/dist /usr/share/nginx/html + +EXPOSE 80 diff --git a/Dockerfile.tenant b/Dockerfile.tenant new file mode 100644 index 0000000..3f29374 --- /dev/null +++ b/Dockerfile.tenant @@ -0,0 +1,31 @@ +# syntax=docker/dockerfile:1.7 +FROM node:22-alpine AS build +WORKDIR /workspace + +COPY package.json ./package.json +COPY yarn.lock ./yarn.lock +COPY tenant-platform ./tenant-platform +COPY docs-site ./docs-site + +ENV YARN_CACHE_FOLDER=/var/cache/yarn + +RUN --mount=type=cache,target=/var/cache/yarn,sharing=locked \ + yarn install --frozen-lockfile + +ARG TENANT_APP_BASE=/ +ARG TENANT_API_BASE_URL=/api + +RUN cd tenant-platform && \ + VITE_APP_BASE=${TENANT_APP_BASE} \ + VITE_API_BASE_URL=${TENANT_API_BASE_URL} \ + yarn build + +RUN cd docs-site && yarn build + +FROM nginx:1.27-alpine + +COPY nginx/tenant.conf /etc/nginx/conf.d/default.conf +COPY --from=build /workspace/tenant-platform/dist /usr/share/nginx/html/tenant +COPY --from=build /workspace/docs-site/docs/.vitepress/dist /usr/share/nginx/html/docs + +EXPOSE 80 diff --git a/Jenkinsfile b/Jenkinsfile index f374dfe..ed6c12d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,6 +3,7 @@ pipeline { parameters { string(name: 'BRANCH', defaultValue: 'main', description: 'Git 分支名') + choice(name: 'APP', choices: ['tenant-platform', 'ops-platform'], description: '要构建的 Web 应用') string(name: 'IMAGE_TAG', defaultValue: 'latest', description: '镜像 Tag') booleanParam(name: 'DEPLOY', defaultValue: true, description: '构建后是否自动部署') } @@ -14,7 +15,6 @@ pipeline { PROD_HOST = '106.54.23.149' PROD_USER = 'ubuntu' COMPOSE_FILE = '/opt/xuqm/deploy/compose.production.yaml' - IMAGE_NAME = 'web' DOCKER_BUILDKIT = '1' } @@ -23,15 +23,38 @@ pipeline { steps { checkout scm } } + stage('Resolve Build Plan') { + steps { + script { + switch (params.APP) { + case 'tenant-platform': + env.IMAGE_NAME = 'tenant-web' + env.DOCKERFILE = 'Dockerfile.tenant' + env.DEPLOY_SERVICE = 'tenant-web' + env.BUILD_ARGS = '--build-arg TENANT_APP_BASE=/ --build-arg TENANT_API_BASE_URL=/api' + break + case 'ops-platform': + env.IMAGE_NAME = 'ops-web' + env.DOCKERFILE = 'Dockerfile.ops' + env.DEPLOY_SERVICE = 'ops-web' + env.BUILD_ARGS = '--build-arg OPS_APP_BASE=/ --build-arg OPS_API_BASE_URL=/api' + break + default: + error("Unsupported APP: ${params.APP}") + } + } + } + } + stage('Docker Build & Push') { steps { withCredentials([string(credentialsId: 'ACR_PASSWORD', variable: 'ACR_PASS')]) { script { - def fullImage = "${ACR_REGISTRY}/${ACR_NAMESPACE}/${IMAGE_NAME}:${params.IMAGE_TAG}" + def fullImage = "${env.ACR_REGISTRY}/${env.ACR_NAMESPACE}/${env.IMAGE_NAME}:${params.IMAGE_TAG}" bat """ - docker login ${ACR_REGISTRY} -u ${ACR_USERNAME} -p %ACR_PASS% + docker login ${env.ACR_REGISTRY} -u ${env.ACR_USERNAME} -p %ACR_PASS% docker pull ${fullImage} || exit 0 - docker build --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from ${fullImage} -t ${fullImage} . + docker build -f ${env.DOCKERFILE} ${env.BUILD_ARGS} --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from ${fullImage} -t ${fullImage} . docker push ${fullImage} docker rmi ${fullImage} || exit 0 """ @@ -45,9 +68,9 @@ pipeline { steps { withCredentials([sshUserPrivateKey(credentialsId: 'PROD_SSH_KEY', keyFileVariable: 'SSH_KEY')]) { script { - def fullImage = "${ACR_REGISTRY}/${ACR_NAMESPACE}/${IMAGE_NAME}:${params.IMAGE_TAG}" + def fullImage = "${env.ACR_REGISTRY}/${env.ACR_NAMESPACE}/${env.IMAGE_NAME}:${params.IMAGE_TAG}" bat """ - ssh -i "%SSH_KEY%" -o StrictHostKeyChecking=no ${PROD_USER}@${PROD_HOST} "docker pull ${fullImage} && docker compose -f ${COMPOSE_FILE} up -d --no-deps web && docker image prune -f" + ssh -i "%SSH_KEY%" -o StrictHostKeyChecking=no ${env.PROD_USER}@${env.PROD_HOST} "docker pull ${fullImage} && docker compose -f ${env.COMPOSE_FILE} up -d --no-deps ${env.DEPLOY_SERVICE} && docker image prune -f" """ } } @@ -56,7 +79,7 @@ pipeline { } post { - success { echo "✅ web:${params.IMAGE_TAG} 构建部署成功" } + success { echo "✅ ${env.IMAGE_NAME}:${params.IMAGE_TAG} 构建部署成功" } failure { echo "❌ 构建失败,请检查日志" } } } diff --git a/Jenkinsfile.ops-web b/Jenkinsfile.ops-web new file mode 100644 index 0000000..3bdab24 --- /dev/null +++ b/Jenkinsfile.ops-web @@ -0,0 +1,65 @@ +pipeline { + agent any + + parameters { + string(name: 'BRANCH', defaultValue: 'main', description: 'Git 分支名') + string(name: 'IMAGE_TAG', defaultValue: 'latest', description: '镜像 Tag') + booleanParam(name: 'DEPLOY', defaultValue: true, description: '构建后是否自动部署') + } + + environment { + ACR_REGISTRY = 'crpi-n44qjpuucgjt8e8c.cn-beijing.personal.cr.aliyuncs.com' + ACR_NAMESPACE = 'xuqmgroup' + ACR_USERNAME = 'xuqinmin12' + PROD_HOST = '106.54.23.149' + PROD_USER = 'ubuntu' + COMPOSE_FILE = '/opt/xuqm/deploy/compose.production.yaml' + DOCKER_BUILDKIT = '1' + IMAGE_NAME = 'ops-web' + DOCKERFILE = 'Dockerfile.ops' + DEPLOY_SERVICE = 'ops-web' + BUILD_ARGS = '--build-arg OPS_APP_BASE=/ --build-arg OPS_API_BASE_URL=/api' + } + + stages { + stage('Checkout') { + steps { checkout scm } + } + + stage('Docker Build & Push') { + steps { + withCredentials([string(credentialsId: 'ACR_PASSWORD', variable: 'ACR_PASS')]) { + script { + def fullImage = "${env.ACR_REGISTRY}/${env.ACR_NAMESPACE}/${env.IMAGE_NAME}:${params.IMAGE_TAG}" + bat """ + docker login ${env.ACR_REGISTRY} -u ${env.ACR_USERNAME} -p %ACR_PASS% + docker pull ${fullImage} || exit 0 + docker build -f ${env.DOCKERFILE} ${env.BUILD_ARGS} --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from ${fullImage} -t ${fullImage} . + docker push ${fullImage} + docker rmi ${fullImage} || exit 0 + """ + } + } + } + } + + stage('Deploy to Production') { + when { expression { return params.DEPLOY } } + steps { + withCredentials([sshUserPrivateKey(credentialsId: 'PROD_SSH_KEY', keyFileVariable: 'SSH_KEY')]) { + script { + def fullImage = "${env.ACR_REGISTRY}/${env.ACR_NAMESPACE}/${env.IMAGE_NAME}:${params.IMAGE_TAG}" + bat """ + ssh -i "%SSH_KEY%" -o StrictHostKeyChecking=no ${env.PROD_USER}@${env.PROD_HOST} "docker pull ${fullImage} && docker compose -f ${env.COMPOSE_FILE} up -d --no-deps ${env.DEPLOY_SERVICE} && docker image prune -f" + """ + } + } + } + } + } + + post { + success { echo "✅ ${env.IMAGE_NAME}:${params.IMAGE_TAG} 构建部署成功" } + failure { echo "❌ 构建失败,请检查日志" } + } +} diff --git a/Jenkinsfile.tenant-web b/Jenkinsfile.tenant-web new file mode 100644 index 0000000..61d9ee6 --- /dev/null +++ b/Jenkinsfile.tenant-web @@ -0,0 +1,65 @@ +pipeline { + agent any + + parameters { + string(name: 'BRANCH', defaultValue: 'main', description: 'Git 分支名') + string(name: 'IMAGE_TAG', defaultValue: 'latest', description: '镜像 Tag') + booleanParam(name: 'DEPLOY', defaultValue: true, description: '构建后是否自动部署') + } + + environment { + ACR_REGISTRY = 'crpi-n44qjpuucgjt8e8c.cn-beijing.personal.cr.aliyuncs.com' + ACR_NAMESPACE = 'xuqmgroup' + ACR_USERNAME = 'xuqinmin12' + PROD_HOST = '106.54.23.149' + PROD_USER = 'ubuntu' + COMPOSE_FILE = '/opt/xuqm/deploy/compose.production.yaml' + DOCKER_BUILDKIT = '1' + IMAGE_NAME = 'tenant-web' + DOCKERFILE = 'Dockerfile.tenant' + DEPLOY_SERVICE = 'tenant-web' + BUILD_ARGS = '--build-arg TENANT_APP_BASE=/ --build-arg TENANT_API_BASE_URL=/api' + } + + stages { + stage('Checkout') { + steps { checkout scm } + } + + stage('Docker Build & Push') { + steps { + withCredentials([string(credentialsId: 'ACR_PASSWORD', variable: 'ACR_PASS')]) { + script { + def fullImage = "${env.ACR_REGISTRY}/${env.ACR_NAMESPACE}/${env.IMAGE_NAME}:${params.IMAGE_TAG}" + bat """ + docker login ${env.ACR_REGISTRY} -u ${env.ACR_USERNAME} -p %ACR_PASS% + docker pull ${fullImage} || exit 0 + docker build -f ${env.DOCKERFILE} ${env.BUILD_ARGS} --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from ${fullImage} -t ${fullImage} . + docker push ${fullImage} + docker rmi ${fullImage} || exit 0 + """ + } + } + } + } + + stage('Deploy to Production') { + when { expression { return params.DEPLOY } } + steps { + withCredentials([sshUserPrivateKey(credentialsId: 'PROD_SSH_KEY', keyFileVariable: 'SSH_KEY')]) { + script { + def fullImage = "${env.ACR_REGISTRY}/${env.ACR_NAMESPACE}/${env.IMAGE_NAME}:${params.IMAGE_TAG}" + bat """ + ssh -i "%SSH_KEY%" -o StrictHostKeyChecking=no ${env.PROD_USER}@${env.PROD_HOST} "docker pull ${fullImage} && docker compose -f ${env.COMPOSE_FILE} up -d --no-deps ${env.DEPLOY_SERVICE} && docker image prune -f" + """ + } + } + } + } + } + + post { + success { echo "✅ ${env.IMAGE_NAME}:${params.IMAGE_TAG} 构建部署成功" } + failure { echo "❌ 构建失败,请检查日志" } + } +} diff --git a/nginx/ops.conf b/nginx/ops.conf new file mode 100644 index 0000000..3aa17e6 --- /dev/null +++ b/nginx/ops.conf @@ -0,0 +1,11 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/nginx/tenant.conf b/nginx/tenant.conf new file mode 100644 index 0000000..bf15e1a --- /dev/null +++ b/nginx/tenant.conf @@ -0,0 +1,16 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html/tenant; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /docs/ { + alias /usr/share/nginx/html/docs/; + try_files $uri $uri/ /docs/index.html; + } +}