From a2c6f57931fe472bb397f35143ea5ba86fa22e7a Mon Sep 17 00:00:00 2001 From: sunlei Date: Fri, 5 Jun 2026 15:04:02 +0800 Subject: [PATCH] =?UTF-8?q?ci:=20=E9=85=8D=E7=BD=AE=E5=8D=9A=E5=AE=A2?= =?UTF-8?q?=E5=89=8D=E5=8F=B0=E5=8F=91=E5=B8=83=E9=93=BE=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile | 186 +++++++++++++++++++++++++++++++++++++++++ deploy/nginx-blog.conf | 35 ++++++++ 2 files changed, 221 insertions(+) create mode 100644 Jenkinsfile create mode 100644 deploy/nginx-blog.conf diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..98bcebc --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,186 @@ +def runCmd(String unixScript, String windowsScript = null) { + if (isUnix()) { + sh unixScript + } else { + bat(windowsScript ?: unixScript) + } +} + +def isPublishBranch(String branchName, String pattern) { + return branchName ==~ pattern +} + +pipeline { + agent { label 'kt-node-agent' } + + options { + skipDefaultCheckout(true) + timestamps() + disableConcurrentBuilds() + buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '10')) + } + + parameters { + booleanParam(name: 'DEPLOY_STATIC_FILES', defaultValue: true, description: '构建成功后是否发布 dist 到 Nginx 静态目录;仅发布分支生效') + string(name: 'PUBLISH_BRANCH_PATTERN', defaultValue: '^(main|master|release/.+)$', description: '允许发布静态文件的分支正则') + string(name: 'DEPLOY_TARGET_DIR', defaultValue: '/home/jenkins/agent/frontends/html/blog', description: 'Nginx 挂载目录中 blog-web 项目的静态文件目录') + string(name: 'VITE_BASE', defaultValue: '/', description: '构建进 blog-web 的 Vite base 路径') + string(name: 'VITE_BLOG_THEME_CONFIG_URL', defaultValue: '/api/blog/theme/config', description: 'Blog 主题配置接口') + string(name: 'VITE_BLOG_ARTICLE_LIST_URL', defaultValue: '/api/blog/article/public/list', description: 'Blog 文章列表接口') + string(name: 'VITE_BLOG_ARTICLE_DETAIL_URL', defaultValue: '/api/blog/article/public/detail', description: 'Blog 文章详情接口') + } + + environment { + CI = 'true' + NODE_OPTIONS = '--max-old-space-size=4096' + PNPM_VERSION = '10.28.2' + } + + stages { + stage('Checkout') { + steps { + checkout scm + } + } + + stage('Prepare') { + steps { + script { + env.IS_CHANGE_REQUEST = env.CHANGE_ID ? 'true' : 'false' + def publishPattern = params.PUBLISH_BRANCH_PATTERN?.trim() ?: '^(main|master|release/.+)$' + env.IS_PUBLISH_BRANCH = (!env.CHANGE_ID && isPublishBranch(env.BRANCH_NAME ?: '', publishPattern)) ? 'true' : 'false' + + if (isUnix()) { + runCmd(""" + node --version + if command -v corepack >/dev/null 2>&1; then + corepack enable + corepack prepare pnpm@${env.PNPM_VERSION} --activate + fi + if ! command -v pnpm >/dev/null 2>&1; then + echo "pnpm or corepack is required on the Jenkins Agent." + exit 1 + fi + pnpm --version + """.stripIndent()) + } else { + runCmd('', """ + node --version + where corepack >nul 2>nul + if not errorlevel 1 ( + corepack enable + corepack prepare pnpm@${env.PNPM_VERSION} --activate + ) + where pnpm >nul 2>nul + if errorlevel 1 exit /b 1 + pnpm --version + """.stripIndent()) + } + + echo """ + Branch: ${env.BRANCH_NAME ?: '-'} + Change request: ${env.CHANGE_ID ?: '-'} + Publish branch: ${env.IS_PUBLISH_BRANCH} + Deploy static files: ${params.DEPLOY_STATIC_FILES} + Deploy target: ${params.DEPLOY_TARGET_DIR} + Vite base: ${params.VITE_BASE} + Theme config URL: ${params.VITE_BLOG_THEME_CONFIG_URL} + Article list URL: ${params.VITE_BLOG_ARTICLE_LIST_URL} + Article detail URL: ${params.VITE_BLOG_ARTICLE_DETAIL_URL} + """.stripIndent() + } + } + } + + stage('Install') { + steps { + script { + runCmd('pnpm install --frozen-lockfile') + } + } + } + + stage('Verify') { + steps { + script { + runCmd('pnpm test:unit -- --run') + } + } + } + + stage('Build') { + steps { + script { + withEnv([ + "VITE_BASE=${params.VITE_BASE}", + "VITE_BLOG_THEME_CONFIG_URL=${params.VITE_BLOG_THEME_CONFIG_URL}", + "VITE_BLOG_ARTICLE_LIST_URL=${params.VITE_BLOG_ARTICLE_LIST_URL}", + "VITE_BLOG_ARTICLE_DETAIL_URL=${params.VITE_BLOG_ARTICLE_DETAIL_URL}", + ]) { + runCmd('pnpm run build') + } + } + } + } + + stage('Deploy Static') { + when { + allOf { + expression { return params.DEPLOY_STATIC_FILES } + expression { return env.IS_CHANGE_REQUEST != 'true' } + expression { return env.IS_PUBLISH_BRANCH == 'true' } + } + } + steps { + script { + if (!isUnix()) { + error('Deploy Static stage requires a Linux/NAS Jenkins Agent.') + } + + def targetDir = params.DEPLOY_TARGET_DIR?.trim() + if (!targetDir) { + error('DEPLOY_TARGET_DIR is required when DEPLOY_STATIC_FILES is enabled.') + } + + // 先发布到临时目录再替换目标目录,避免 Nginx 读到半复制状态。 + withEnv(["TARGET_DIR=${targetDir}"]) { + runCmd(""" + set -e + test -f dist/index.html + + case "\${TARGET_DIR}" in + ""|"/"|"/home"|"/home/jenkins"|"/home/jenkins/agent"|"/usr"|"/usr/share"|"/usr/share/nginx"|"/usr/share/nginx/html") + echo "Unsafe DEPLOY_TARGET_DIR: \${TARGET_DIR}" + exit 1 + ;; + esac + + parent_dir=\$(dirname "\${TARGET_DIR}") + release_dir="\${TARGET_DIR}.release-${env.BUILD_NUMBER}" + previous_dir="\${TARGET_DIR}.previous" + + mkdir -p "\${parent_dir}" + rm -rf "\${release_dir}" "\${previous_dir}" + mkdir -p "\${release_dir}" + cp -a dist/. "\${release_dir}/" + + if [ -d "\${TARGET_DIR}" ]; then + mv "\${TARGET_DIR}" "\${previous_dir}" + fi + mv "\${release_dir}" "\${TARGET_DIR}" + rm -rf "\${previous_dir}" + + find "\${TARGET_DIR}" -maxdepth 2 -type f | head + """.stripIndent()) + } + } + } + } + } + + post { + success { + archiveArtifacts artifacts: 'dist/**,package.json,pnpm-lock.yaml,Jenkinsfile,deploy/**', fingerprint: true, allowEmptyArchive: true + } + } +} diff --git a/deploy/nginx-blog.conf b/deploy/nginx-blog.conf new file mode 100644 index 0000000..23ceb5b --- /dev/null +++ b/deploy/nginx-blog.conf @@ -0,0 +1,35 @@ +server { + listen 48091; + server_name _; + + root /usr/share/nginx/html/blog; + index index.html; + + gzip on; + gzip_min_length 1024; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml; + + location = /index.html { + add_header Cache-Control "no-store"; + try_files $uri =404; + } + + location ^~ /api/ { + proxy_pass http://192.168.31.224:48085/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location ~* \.(js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + location / { + try_files $uri $uri/ /index.html; + } +}